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:

The failure — a condition in the world. The disk is full. The token expired. The peer hung up. It exists whether or not your program notices it.
The error value — the in-program object that reifies that condition so it can be returned, matched on, logged, and wrapped. 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.

Working definition. An error model is a language’s set of rules for three things: how failures become values, how those values move, and what the consumer is forced to do with them. Those are the three axes. Framing after Gandhi 2025.

Axis 1 — Representation

What kind of thing is the error value?

Error code — an integer or enum in a side channel or return slot. C's errno, POSIX -1.
Tagged union — the success and failure cases are alternatives of one type, so you cannot read the value without confronting the failure. Rust Result<T, E>, Zig error unions, Haskell Either.
Option — a tagged union whose failure case carries nothing. Option<T>, None.
Thrown object — a heap value handed to the runtime rather than returned. Python, Java, C++, Ruby.
Interface value — an open set: anything satisfying a one-method contract counts. Go's error. This is a genuinely distinct point on the axis: unlike a tagged union, the set of possible errors is not closed.
Nothing at all — a bare signal. Pony's 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?

Implicit / silent — propagation is the default and is invisible at the call site. In Python or Java, 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.
Explicit at the call site — every fallible call is syntactically marked. Rust's ?, Swift's try, Go's if err != nil { return err }. The marker is at the call, so the reader sees where control can leave.
Declared in the signature — the function's type names what it can fail with. Java's throws IOException, Nim's raises. Note this is orthogonal to the previous row: Java has signature declaration and silent call sites.
Non‑propagating — the failure does not travel; the process ends. Abandonment, from lesson 01. Gandhi calls this axis position fail‑fast, against fail‑slow for everything above.

Axis 3 — Handling

At the consumption site, what does the language force?

Exhaustive — every failure case must be matched or the program does not compile. Rust's match on a closed enum; Zig error sets. The cost: adding a variant breaks every consumer.
Non‑exhaustive — a closed match is forbidden; a catch‑all arm is required, so new cases can be added compatibly. Rust #[non_exhaustive], Swift @unknown default. This is the direct answer to the versioning problem — hold that thought, it returns when we do checked exceptions.
Unchecked — nothing is forced. Python's 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:

LanguageRepresentationPropagationHandling
Pythonthrown object, rich metadataimplicitunchecked
Javathrown objectimplicit at call site, declared in signatureunchecked at the catch
Gointerface value, string metadataexplicit, manualunchecked
Rusttagged union, structured metadataexplicit (?)exhaustive by default
Swiftthrown objectexplicit (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

Reification — turning a condition in the world into an in‑program value that can be passed around.
Representation — what kind of thing the error value is. Gandhi 2025
Propagation — how the value travels to its handler; implicit, explicit at the call site, declared in the signature, or not at all. Gandhi 2025
Handling — what the consumption site is forced to do: exhaustive, non‑exhaustive, or unchecked. Gandhi 2025
Metadata — the payload the value carries: structured fields, a string, or nothing. Determines whether a caller can act or only report.
Granularity — the scope a handling construct covers: expression, statement, function, package.
Fail‑fast vs. fail‑slow — Gandhi's names for the non‑propagating and propagating positions on axis 2.
Second great convergence — the claim that Go, Rust, Swift and Zig have settled on first‑class error values + call‑site‑marked propagation + a separate panic channel. Kladov 2025

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.

← Lesson 01 Status map Lesson 03 →

Sources: Gandhi, An epic treatise on error models · Kladov, The Second Great Error Model Convergence · Duffy, The Error Model · RESOURCES.md