Error handling
On this page
Xo has two kinds of failure, and the language keeps them apart:
- an error is something the caller can reasonably handle (bad input, a
missing file). It is a value in the return type after
!. - a fault is a bug (a broken
requires, an index out of range). It stops the task, and only an isolation boundary turns it into a value.
Layered errors with from
Each layer has its own error enum. A from variant lets ? convert a lower
layer’s error into the upper one.
errors-chain/main.xo
enum ParseErr {
Empty
NotANumber(text: Str)
}
fn (e: ParseErr) display() -> Str {
match e {
Empty => "empty input"
NotANumber(text) => "not a number: ${text}"
}
}
enum RangeErr {
TooSmall(n: Int, min: Int)
TooBig(n: Int, max: Int)
}
fn (e: RangeErr) display() -> Str {
match e {
TooSmall(n, min) => "${n} is below ${min}"
TooBig(n, max) => "${n} is above ${max}"
}
}
/// The error of `port`: either layer's error converts into it, because of `from`.
enum PortErr {
Parse(from ParseErr)
Range(from RangeErr)
}
fn (e: PortErr) display() -> Str {
match e {
Parse(p) => "bad port text: ${p.display()}"
Range(r) => "bad port number: ${r.display()}"
}
}
fn parse(text: Str) -> Int ! ParseErr {
if text.trim() == "" {
return Err(ParseErr.Empty)
}
Int.parse(text.trim()).or_err(ParseErr.NotANumber(text))?
}
fn check_range(n: Int) -> Int ! RangeErr {
if n < 1024 {
return Err(RangeErr.TooSmall(n: n, min: 1024))
}
if n > 65535 {
return Err(RangeErr.TooBig(n: n, max: 65535))
}
n
}
/// Each `?` returns early with the error converted to `PortErr`.
fn port(text: Str) -> Int ! PortErr {
let n = parse(text)?
check_range(n)?
}
fn main(os: Os) {
for text in ["8080", "", "http", "80", "70000"] {
match port(text) {
Ok(p) => os.stdio.println("port ${p}")
Err(e) => os.stdio.println("error: ${e.display()}")
}
}
}
test "the error says which layer failed" {
match port("80") {
Ok(_) => fault("expected an error")
Err(Range(TooSmall(n, _))) => expect(n) == 80
Err(e) => fault("unexpected: ${e.display()}")
}
}Output:
port 8080
error: bad port text: empty input
error: bad port text: not a number: http
error: bad port number: 80 is below 1024
error: bad port number: 70000 is above 65535
portreturnsInt ! PortErr.parse(text)?yields aParseErr, which theParse(from ParseErr)variant wraps;check_range(n)?does the same forRangeErr.- The caller can still tell which layer failed: the test matches
Err(Range(TooSmall(n, _))). - Variants with two fields are built with named arguments.
Ignoring a result is not allowed to pass silently:
fn parse_port(s: Str) -> Int ! Error {
Int.parse(s).or_err("not a number")?
}
fn main(os: Os) {
parse_port("8080")
}xo check says:
main.xo:6:3: error XO0201: result of `parse_port` is not handled
note: `parse_port` can fail with `Error`; propagate with `?`, match on it, or discard it with `.ignore_err("reason")` (core 2.6)
fix: discard with a reason
1 module(s), 1 error(s), 0 warning(s)
Handling an error without ceremony
errors-handling/main.xo
fn lookup(env: Map[Str, Str], key: Str) -> Str? {
env.get(key)
}
/// `??` takes the value, or the right side when there is none.
fn port_or_default(env: Map[Str, Str]) -> Int {
let text = lookup(env, "PORT") ?? return 8080
Int.parse(text) ?? 8080
}
/// Turn a missing value into an error with `or_err`, and pass errors up with `?`.
fn strict_port(env: Map[Str, Str]) -> Int ! Error {
let text = lookup(env, "PORT").or_err("PORT is not set")?
Int.parse(text).or_err("PORT is not a number: ${text}")?
}
/// `r ?? e => body` handles the error and gives the fallback.
fn port_logged(out: Stdio, env: Map[Str, Str]) -> Int uses stdio {
strict_port(env) ?? e => {
out.println("falling back: ${e.display()}")
8080
}
}
fn main(os: Os) {
let cases: List[Map[Str, Str]] = [{"PORT": "9000"}, {"PORT": "x"}, {}]
for env in cases {
os.stdio.println("default: ${port_or_default(env)}")
os.stdio.println("logged: ${port_logged(os.stdio, env)}")
match strict_port(env) {
Ok(p) => os.stdio.println("strict: ${p}")
Err(e) => os.stdio.println("strict failed: ${e.display()}")
}
if let Some(p) = lookup(env, "PORT") {
os.stdio.println("PORT is set to ${p}")
}
}
}
test "the fallback only applies on an error" {
expect(port_or_default({"PORT": "9000"})) == 9000
expect(port_or_default({"PORT": "x"})) == 8080
expect(port_or_default({})) == 8080
}Output:
default: 9000
logged: 9000
strict: 9000
PORT is set to 9000
default: 8080
falling back: PORT is not a number: x
logged: 8080
strict failed: PORT is not a number: x
PORT is set to x
default: 8080
falling back: PORT is not set
logged: 8080
strict failed: PORT is not set
a ?? bgives the value, orbwhen there is none. With aResultthe right side may bind the error:r ?? e => { ... }.?? return xleaves the function early when the value is missing.or_errturns anOptioninto aResult;?passes the error up.if let Some(p) = ...handles just the case you care about.
Faults are not errors
errors-faults/main.xo
use std/task
/// A bad index is a bug in the caller, not an error to handle: it is a fault.
fn nth(xs: List[Int], i: Int) -> Int
requires i >= 0 && i < xs.len()
{
xs[i]
}
/// Bad input from outside is an error: the caller decides what to do.
fn nth_checked(xs: List[Int], i: Int) -> Int ! Error {
if i < 0 || i >= xs.len() {
return Err("index ${i} out of range for ${xs.len()} items")
}
xs[i]
}
fn main(os: Os) {
let xs = [10, 20, 30]
os.stdio.println("nth: ${nth(xs, 1)}")
match nth_checked(xs, 7) {
Ok(v) => os.stdio.println("got ${v}")
Err(e) => os.stdio.println("error: ${e.display()}")
}
// `task.isolate` runs code in its own scope and turns a fault into a value
match task.isolate(fn() -> Int ! Error { Ok(nth(xs, 7)) }) {
Ok(v) => os.stdio.println("got ${v}")
Err(Faulted(f)) => os.stdio.println("fault caught: ${f.msg}")
Err(Failed(e)) => os.stdio.println("error: ${e.display()}")
}
}Output:
nth: 20
error: index 7 out of range for 3 items
fault caught: requires failed: i >= 0 && i < xs.len()
nthstates its precondition withrequires. Breaking it is the caller’s bug, so it is a fault, not an error value.nth_checkedtakes input that may legitimately be wrong, so it returns an error.task.isolateruns a function in its own scope and returnsFaultedwhen it faults. Use it at a service boundary (one request), not to hide bugs.
What to take away
- Input from outside is an error; a broken promise inside the program is a fault.
- Give each layer its own error enum and connect them with
from. - Prefer
??for fallbacks andmatchwhen each case needs its own action.
Back to the book overview.
↑ index