Errors
Goal: treat failure as a value (
Resultand error-sets), propagate withcatch(no?operator), and get to knownoreturn.
Two axes: Result and error{...}
Section titled “Two axes: Result and error{...}”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).
Match the Result (fork)
Section titled “Match the Result (fork)”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()}Trailers: catch and match |e| (no ?)
Section titled “Trailers: catch and match |e| (no ?)”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) } // abortcatch |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 linenoreturn: divergence
Section titled “noreturn: divergence”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