Packages

result

Build results, chain them with andThen, orElse and mapErr, and turn failures into fallbacks, without a pile of nested checks.

import "std:result"

A result<T, E> is the outcome of something that can fail: either ok with a value of type T, or err with an error E. The errors lesson shows how to receive one with catch, switch and !. This page is the other half: making your own results, and the methods that let you transform and combine them without unwrapping them first.

The type is built into the language, but std:result provides the constructors result.ok, result.err and result.fromGo, so import it where you make results.

Making results#

result.ok(value) is a success and result.err(error) is a failure. error("message") makes an error value, and error("Kind", "message") gives it a kind you can test:

main.osl
import "std:result"

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

log divide(10, 4).unwrap()

auto failed = divide(1, 0)
log failed.isErr()
log failed.unwrapErr().kind
log failed.unwrapErr().message
Click the code to edit it

Return result<T, error> from any function whose failure is part of normal operation. Callers must then decide what a failure means.

Reading a result#

MethodOn okOn err
isOk() isErr()true falsefalse true
unwrap()the valuethrows the error
unwrap("message")the valuethrows with your message
unwrapOr(fallback)the valuethe fallback
unwrapOrElse(fn)the valuewhat fn(error) returns
unwrapErr()throws a StateErrorthe error

unwrapOrElse is unwrapOr for when the fallback depends on the error, or is costly to compute, since the function only runs on failure:

main.osl
import "std:result"

def port(string text) result<int, error> (
  int n = text.tryAs<int> catch (e) (
    return result.err(error("ParseError", "not a number: " ++ text))
  )
  return result.ok(n)
)

log port("8080").unwrapOr(80)
log port("http").unwrapOr(80)
log port("http").unwrapOrElse(e -> e.message.len)
log try(port("http").unwrap("config needs a port")).unwrapErr().message
Click the code to edit it

expect(message) is the same as unwrap(message): use either to say what you were counting on when the program stops.

Chaining with andThen#

Suppose each step can fail, and each needs the last step's value. Unwrapping every result and checking it gets deep fast. andThen(fn) runs fn on the value if the result is ok, and fn returns the next result. A failure anywhere skips every step after it:

main.osl
import "std:result"

def parseNumber(string text) result<int, error> (
  int n = text.tryAs<int> catch (e) (
    return result.err(error("ParseError", "not a number: " ++ text))
  )
  return result.ok(n)
)

def checkPositive(int n) result<int, error> (
  if n <= 0 return result.err(error("RangeError", "must be positive"))
  return result.ok(n)
)

def parsePositive(string text) result<int, error> (
  return parseNumber(text).andThen(checkPositive).andThen(n -> result.ok(n * 10))
)

log parsePositive("4").unwrap()
log parsePositive("-4").unwrapErr().message
log parsePositive("four").unwrapErr().message
Click the code to edit it

The ! operator from the errors lesson does the same job as statements, and reads better when you need the intermediate values. Use andThen for a short pipeline of functions.

Recovering with orElse#

orElse(fn) is the opposite: it runs only on failure, and fn(error) returns a new result, so you can try a backup:

main.osl
import "std:result"

def fromEnv(string name) result<string, error> (
  return result.err(error("NotFound", name ++ " is not set"))
)

def fromDefault(error e) result<string, error> (
  return result.ok("localhost")
)

string host = fromEnv("HOST").orElse(fromDefault).unwrap()
log host
Click the code to edit it

Chain several orElse calls to try a list of sources in order.

Changing the error with mapErr#

mapErr(fn) transforms the error and leaves a success alone. Use it to add context as an error travels up, so "not a number: four" becomes "reading port: not a number: four":

main.osl
import "std:result"

def parse(string text) result<int, error> (
  int n = text.tryAs<int> catch (e) (
    return result.err(error("ParseError", "not a number: " ++ text))
  )
  return result.ok(n)
)

auto r = parse("four").mapErr(e -> error("ConfigError", "reading port: " ++ e.message))
log r.unwrapErr().kind
log r.unwrapErr().message
Click the code to edit it

Errors have a kind#

An error value has a kind (a short category) and a message. Callers can branch on the kind and leave the message for people:

main.osl
import "std:result"

def load(string name) result<string, error> (
  if name == "" return result.err(error("ValueError", "name is empty"))
  if name == "ghost" return result.err(error("NotFoundError", "no such file"))
  return result.ok("contents of " ++ name)
)

for name in ["notes", "", "ghost"] (
  switch load(name) (
    case ok(text)
      log text
    case err(e)
      log e.kind
  )
)
Click the code to edit it

Results without a value#

Some operations succeed with nothing to report, such as saving a file. Leave the type empty, and make the success with result.ok():

main.osl
import "std:result"

def save(string name) result<, error> (
  if name == "" return result.err(error("ValueError", "a name is required"))
  return result.ok()
)

log save("report").isOk()
log save("").isErr()
Click the code to edit it

Working with a list of results#

Results are ordinary values, so they live in arrays. Typed results in an array let you count the failures and gather the successes:

main.osl
import "std:result"

def half(int n) result<int, error> (
  if n % 2 != 0 return result.err(error("ValueError", n ++ " is odd"))
  return result.ok(n / 2)
)

result<int, error>[] outcomes = [half(4), half(7), half(10), half(3)]

log outcomes.filter(r -> r.isOk()).map(r -> r.unwrap())
log outcomes.filter(r -> r.isErr()).len
log outcomes.map(r -> r.unwrapOr(0)).sum()
Click the code to edit it

Turning a throw into a result#

try(expression) runs code that might throw and gives back a result. It is how you fit a function that throws into a pipeline:

main.osl
import "std:result"

def risky(int n) int (
  if n < 0 throw error("RangeError", "negative")
  return n * 2
)

auto good = try(risky(4))
auto bad = try(risky(-1))
log good.unwrap()
log bad.unwrapErr().kind
Click the code to edit it

Results from Go#

When you call Go code, a function returns a value and an error as two things. result.fromGo(value, error) joins them: ok(value) when the error is null, otherwise err. A text error written Kind: message keeps its kind:

main.osl
import "std:result"

auto good = result.fromGo(42, null)
log good.unwrap()

auto bad = result.fromGo(0, "NotFoundError: gone")
log bad.unwrapErr().kind
log bad.unwrapErr().message
Click the code to edit it
Quick check

What does this print?

osl
auto r = result.ok(4).andThen(n -> result.err(error("ValueError", "too big")))
log r.isOk()
Your turn

A safe sum

Write total(string[] texts), which returns a result holding the sum of the whole numbers in texts, or the first failure. Use andThen. The program prints -1 for the list with a bad entry.

main.osl
Ctrl Enter runs
Expected output
6
-1

API reference#

3 members, generated from the std:result source of OSL 9.3.0.

Functions

#result.err(any e) result

err returns a failed result holding e, usually an error such as error("NotFoundError", "no such user").

#result.fromGo(any val, any err) result

fromGo turns a Go value and error pair into a result: ok(val) when err is null, otherwise a failed result whose error is typed. A Go error gets the kind that describes it, a "Kind: message" string keeps its kind, and anything else is a RuntimeError.

#result.ok(any v) result

ok returns a successful result holding v.