Error Handling · Lesson 02
Three Questions Every Error Model Answers
Lesson 01 told you which failures deserve handling. This one gives you the frame for comparing how — so that “exceptions versus errors‑as‑values” stops being one argument and becomes three separate ones.
A failure is not an error value
Two things are routinely called “the error” and they are not the same object:
ErrNotFound,
io.IOError("disk full"),
Err(ParseError { line: 12 }).Reification is not automatic and not always complete. A segfault is a failure with no error value at
all. Option<T> reifies a failure with an
empty payload — you learn that it failed and nothing about why. C's
errno reifies into a global integer that the next call may
overwrite.
Axis 1 — Representation
What kind of thing is the error value?
errno, POSIX -1.Result<T, E>,
Zig error unions, Haskell Either.Option<T>, None.error. This is a genuinely distinct point on the axis:
unlike a tagged union, the set of possible errors is not closed.error
carries no payload whatsoever.Gandhi splits a sub‑axis out of this one, metadata: given that there is a value, what does it
carry? Structured typed fields (a Rust enum variant with a line: u32),
an unstructured string (fmt.Errorf), a whole object with methods
and a stack trace (Java), or nothing. Metadata is what determines whether a caller can act on the failure or
only report it.
Axis 2 — Propagation
How does the value get from the site of failure to the code that handles it?
x = f() may or may not return; you cannot
tell by looking. Gandhi's phrasing: nearly any function may throw any exception without explicit marking.?, Swift's try,
Go's if err != nil { return err }. The marker is at the
call, so the reader sees where control can leave.throws IOException, Nim's
raises. Note this is orthogonal to the previous row:
Java has signature declaration and silent call sites.Axis 3 — Handling
At the consumption site, what does the language force?
match on a closed enum; Zig error sets.
The cost: adding a variant breaks every consumer.#[non_exhaustive],
Swift @unknown default. This is the direct answer to the
versioning problem — hold that thought, it returns when we do checked exceptions.except clauses are checked by nobody; Go's
err can be assigned to _.Gandhi's fifth axis, granularity, asks what scope the handling construct covers:
a single expression (Swift's try expr), a statement block
(try { … }), a whole function (Java's
throws), or a whole package. Granularity is why a broad
try block is a smell: it applies one decision to twenty
failure sites that deserved different ones.
Exercise 1 · Recognition
Which axis does this feature vary?
These are deliberately interleaved so that adjacent items come from different axes. Discriminating them under interleaving is what makes the distinction stick.
The payoff: the axes are independent
“Exceptions versus errors‑as‑values” is treated as a single choice, and it is not. It is at least two choices that happen to be correlated in the languages people learned first:
| Language | Representation | Propagation | Handling |
|---|---|---|---|
| Python | thrown object, rich metadata | implicit | unchecked |
| Java | thrown object | implicit at call site, declared in signature | unchecked at the catch |
| Go | interface value, string metadata | explicit, manual | unchecked |
| Rust | tagged union, structured metadata | explicit (?) | exhaustive by default |
| Swift | thrown object | explicit (try) | non‑exhaustive |
Swift is the row that breaks the false dichotomy: it throws, and it is explicit at the call site. Representation and propagation came apart. Once you can see that, an argument like “exceptions make control flow invisible” becomes precise — it is a complaint about propagation only, and it does not license any conclusion about representation.
Kladov (2025) argues the modern languages have converged on one specific set of coordinates: error values are first‑class, propagation is marked at the call site, and bugs get a separate channel (panic) rather than sharing the exception mechanism. That last point is lesson 01 expressed as a language feature: Duffy's cut, made syntactic.
Exercise 2 · Execution
Plot the fragment
Read each fragment and give its coordinates. Do it from the code, not from what you remember about the language — one of these is a language behaving unlike its reputation.
Exercise 3 · Recall
From memory
Write before revealing.
Glossary — added this lesson
What this unlocks
Two branches open. Going down the representation axis: once errors are values, how do you let a caller distinguish them without welding your internals to their code? That is Cheney's sentinel / type / opaque trichotomy — lesson 03. Going down the propagation axis: if control can leave a function at any marked point, what state is the function's data in when it does? That is Abrahams' exception safety — lesson 04.
Sources: Gandhi, An epic treatise on error models · Kladov, The Second Great Error Model Convergence · Duffy, The Error Model · RESOURCES.md