Errors
The redefinition: an error is a value, not a control event.
Result[T,E]+ error-sets, explicit propagation (no?), and nounit.
The idea
Section titled “The idea”There are two big schools for failure. Exceptions (Java/Python/C++): the error is an event that “explodes” and rises up the stack until someone catches it, invisible in the function’s type. Error-as-value (Go/Rust): the failure is a return value that you inspect. Makoto stands firmly in the second school, and radicalizes the explicitness.
How Makoto redefines it
Section titled “How Makoto redefines it”Failure has two questions, and there is a construct for each:
Result[T, E]is the resolution:Ok(T)orErr(E). It is a family of arity 2, likeMap[K,V], so it composes with generics (mapover theOk,collectofList[Result[T]]toResult[List[T]]). Whoever receives aResultcannot ignore the failure: it is in the type.error{...}is the union: which failures exist. Closed (error{A, B}, exhaustive, forbids_), open (bareerror, requires_), or empty (error{}= “does not err”,Errunconstructible).
Propagation is explicit, by trailer, never by operator: v := do() catch |e| { return e } propagates;
catch |e| treats the error as a unit; match |e| branches by variant in one shot (it binds and
discriminates, without the double-binding “first catch, then match”). And success-without-value does not use
unit: it is the error-set alone (fn send(m) -> error{...}), where success = absence of error.
noreturn is the bottom type of divergence (panic/exit), which makes an error arm “escape” without giving a
value.
The reasoning
Section titled “The reasoning”The declared inspiration is Go (forcing the recovery tree to be drawn line by line), but with
a twist: instead of repeating if err != nil, you write catch/match |e| once, and the
compiler demands that you handle or propagate on purpose. The central gain is killing the hidden flow:
you never propagate an error by accident; each step is a visible decision (“handle here or pass it on?”).
The choice against unit is pure conceptual economy: do not multiply empty types. If success carries no
value, the error-set alone already says everything; if you need success as a composition value,
Result[(...), E] solves it, but the common case (a procedure that can fail) does not pay for a phantom
type.
Why not the alternatives
Section titled “Why not the alternatives”- Exceptions. Hidden control flow: you call
fetch_user(id)and it can ejectNetworkError/ParseError/Timeoutto any level of the stack, with nothing in the type. The failure contract stays invisible. Error-as-value makes it part of the signature. - The
?operator (Rust/Zig). It is the same danger as uncontainable coloring, applied to errors: it becomes the way to escape handling the failure, a glyph that propagates by itself. The language should make the cost of propagation visible;catch |e| { return e }is longer on purpose, and it is that friction that kills negligence. (Note the parallel withunsafe: the difference is thatunsafeis containable and?only hides.) unit/voidas a type. An empty type that carries no information; the bare error-set takes its place without the phantom.-> (T, error)raw Go style. It allows the ambiguousnil, niland does not compose with generics. Makoto pushes toward the canonical form:-> (u8, error)generates a “useResult[u8, error]” warning.
The concrete pain
Section titled “The concrete pain”The function in Java whose throws lies (or does not exist), and the NullPointerException that rises from six
layers below. The unwrap()/? in Rust that becomes a habit and swallows failures in code that should be
robust. The repeated if err != nil of Go that is annoying but (it is acknowledged) works, because it forces
you to look at each failure. Makoto wants the Go effect (mandatory attention to each failure) without the repetition
and without the ? escape door.
The mental model
Section titled “The mental model”An error is a value that you handle or propagate, explicitly. Failure is an exit of the program, not an event that the runtime resolves for you.
The accepted friction
Section titled “The accepted friction”Verbosity is the price, and it is deliberate: you do not chain do_a()?.do_b()?.do_c()?; you assemble the
recovery tree step by step. The spread of error-sets (error{open..., parse...}) adds a bit of
machinery, but it is what avoids re-enumerating the whole list of variants of each subsystem. The bet:
each line that propagates costing to be written is what guarantees that no failure is forgotten.
Next: 06 · Absence