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:
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().messageReturn result<T, error> from any function whose failure is part of normal operation. Callers must then decide what a failure means.
Reading a result#
| Method | On ok | On err |
|---|---|---|
isOk() isErr() | true false | false true |
unwrap() | the value | throws the error |
unwrap("message") | the value | throws with your message |
unwrapOr(fallback) | the value | the fallback |
unwrapOrElse(fn) | the value | what fn(error) returns |
unwrapErr() | throws a StateError | the error |
unwrapOrElse is unwrapOr for when the fallback depends on the error, or is costly to compute, since the function only runs on failure:
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().messageexpect(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:
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().messageThe ! 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:
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 hostChain 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":
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().messageErrors 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:
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
)
)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():
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()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:
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()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:
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().kindResults 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:
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().messageWhat does this print?
auto r = result.ok(4).andThen(n -> result.err(error("ValueError", "too big")))
log r.isOk()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.
6 -1
API reference#
3 members, generated from the std:result source of OSL 9.3.0.
Functions
result.err(any e) resulterr returns a failed result holding e, usually an error such as error("NotFoundError", "no such user").
result.fromGo(any val, any err) resultfromGo 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) resultok returns a successful result holding v.