Learn
Lesson 16

Errors

Handle things that can go wrong with result values, catch, and !, and know when to throw or exit.

Files go missing, networks drop and people type nonsense. OSL's answer is to make failure visible: an operation that can fail returns a result, which is either ok with a value or err with an error. You can't use the value without deciding what happens when there isn't one.

Results#

Converting text to a number can fail, so .tryAs<int> returns a result:

main.osl
auto good = "42".tryAs<int>
auto bad = "forty-two".tryAs<int>
log good.isOk() bad.isErr()
log good.unwrap()
log bad.unwrapOr(0)
Click the code to edit it
  • isOk() and isErr() say which it is.
  • unwrap() gives the value, and stops the program if there isn't one. Use it when failure really can't happen, or would be a bug.
  • unwrapOr(fallback) gives the value, or the fallback for an error.

Looking inside with switch#

switch and match take ok and err cases and pull out what's inside:

main.osl
def parseAge(string text) string (
  switch text.tryAs<int> (
    case ok(age)
      return "age " ++ age
    case err(e)
      return "not a number: " ++ text
  )
)

log parseAge("36")
log parseAge("old")
Click the code to edit it

catch#

catch after a call that returns a result handles the failure right there, and gives back the plain value when it succeeds. Its block decides what happens instead, usually with return, continue or break:

main.osl
import "std:json"

string[] inputs = ["[1, 2, 3]", "not json", "[10]"]
for text in inputs (
  int[] values = json.parse<int[]>(text) catch (e) (
    log "skipping: " ++ e.kind
    continue
  )
  log values.sum()
)
Click the code to edit it

When the handler is one statement, it can follow catch directly: json.parse<int[]>(text) catch continue. (e) names the error; leave it out when you don't need it.

A handler can also finish with a value, which becomes the result of the whole expression, so a failure turns into a fallback:

main.osl
string[] inputs = ["12", "twelve"]
for text in inputs (
  int n = text.tryAs<int> catch (
    log "not a number: " ++ text
    -1
  )
  log n
)
Click the code to edit it

Errors have a kind, such as ParseError or TypeError, and a message that explains what went wrong.

Passing failure up with !#

Often the function that hits an error isn't the right place to handle it. Put ! after a result inside a function that returns a result, and a failure returns from the function straight away, handing the error to the caller. On success, ! gives the plain value:

main.osl
import "std:json"
import "std:result"

struct Config (
  string name = ""
  int port = 0
)

def loadPort(string text) result<int, error> (
  Config c = json.parse<Config>(text)!
  if c.port == 0 return result.err(error("no port set"))
  return result.ok(c.port)
)

log loadPort(`{"name": "web", "port": 8080}`).unwrap()
log loadPort(`{"name": "web"}`).unwrapErr().message
log loadPort("{").isErr()
Click the code to edit it

result.ok(value) and result.err(error) build results, from std:result. error("message") makes an error value.

Writing functions that can fail#

Return result<T, error> from any function whose failure is part of normal life, and the compiler makes every caller deal with it:

main.osl
import "std:result"

def divide(number a, number b) result<number, error> (
  if b == 0 return result.err(error("can't divide by zero"))
  return result.ok(a / b)
)

log divide(10, 4).unwrap()
log divide(1, 0).unwrapOr(-1)
Click the code to edit it

Ignored failures warn#

Many package calls return results. If you use one as a plain value, OSL unwraps it for you but warns, so an unhandled failure is never silent. The fix is to say what you mean: handle it with catch or switch, pass it up with !, or .unwrap() it on purpose.

The compiler also reads your guards. After if !fs.exists(path) return, it knows reading path won't fail for that reason, and stops warning about it.

throw, for the truly exceptional#

throw stops the program with an error, unless something catches it. Keep it for bugs and situations the program can't sensibly continue from; for expected failures, return a result.

main.osl
def checkedAge(int age) int (
  if age < 0 throw "age can't be negative"
  return age
)

log checkedAge(30)
log checkedAge(-1)
log "never reached"
Click the code to edit it

An uncaught throw prints the error, the line that threw it, and the line that called it, then exits with a failure status.

try(expression) turns anything that throws into a result, so you can handle it with the same tools: try(checkedAge(-1)).isErr() is true.

Ending the program#

exit stops the program with an exit status. By convention, 0 means success and anything else means failure, which scripts and other programs can check:

main.osl
string[] args = []
if args.len == 0 (
  error "usage: greet <name>"
  exit 2
)
Click the code to edit it

error prints to the error stream, as log does to the normal one.

Quick check

Inside def load() result<Config, error>, what does json.parse<Config>(text)! do when the JSON is invalid?

Your turn

Sum the valid numbers

Add up the entries that are whole numbers and count how many weren't.

main.osl
Ctrl Enter runs
Expected output
20 2
0 of 24 lessons done