Skip to content

The book · 04

Errors

book.md · 94 lines · 2 min read

Goal: treat failure as a value (Result and error-sets), propagate with catch (no ? operator), and get to know noreturn.

Failure has two questions, and Makoto gives a construct for each:

  • Result[T, E]: the resolution, that is, it worked (Ok(T)) or it didn’t (Err(E)).
  • error{...}: the union of errors, that is, which failures exist (a closed, structural error-set).

The two compose: the E of a Result is an error{...}.

fn open(path: string) -> Result[File, error{NotFound, PermissionDenied}] { ... }

A bare error is open (FFI/third parties, requires _ in the match); error{A, B} is closed (exhaustive, forbids _); error{} is the empty set, that is, “does not err” (a Result[T, error{}] whose Err is unconstructible, useful when a concrete type satisfies a fallible interface but never fails).

Matching the whole Result covers Ok and each error variant, exhaustively:

match open(path) {
Ok(f) => use(f)
Err(error.NotFound) => criar_default()
Err(error.PermissionDenied) => abortar()
}

In the sequential case, when success keeps going and only the error needs attention, use the trailers. The Ok is resolved by the assignment; the trailer deals only with the error. There is no ? operator: propagation is explicit.

v := do_it(x) catch |e| { return Err(e) } // propagate (explicit)
v := do_it(x) catch |e| { return e } // sugar: 'return e' → Err(e)
v := do_it(x) catch |e| { runtime.panic(e) } // abort

catch |e| treats the error as a unit; match |e| branches by variant (only makes sense with a union error):

cfg := load() match |e| { // Ok → cfg; each error arm ESCAPES
error.NotFound => return new_config() // early-return per variant
error.Corrupted => runtime.panic("unreadable config")
}

In value-binding, each arm of the trailer escapes (return/break/panic): the trailer does not give a value to v. When you want the error to produce a value (a default that becomes v), the right construct is match over the whole Result (the fork above), not the trailer.

Composing error-sets is uniting them, with the name... spread:

fn load(p: string) -> Result[Data, error{open..., parse...}] { // unites the variants of open and parse
f := open(p) catch |e| { return e }
d := parse(f) catch |e| { return e }
return Ok(d)
}

Success without a value: the error-set alone

Section titled “Success without a value: the error-set alone”

Since there is no unit, an operation that succeeds without producing a value returns just the error-set:

fn send(msg: Msg) -> error{ProcessDown} // success = absence of error
chan <- data catch |e| { return e } // error → propagate; success → falls to the next line

A function that never returns control has the bottom type noreturn. It is what lets panic/exit serve as an “escape” in a catch in value-binding (the error path diverges, so v is still the type of Ok):

fn panic(msg: string) -> noreturn // runtime.panic; same for os.exit, infinite event-loops
x := parse(s) catch |e| { runtime.panic(e) } // x types as int: the error arm diverges

Next: 05 · Collections and strings