Error Handling · Lesson 01

Bugs Are Not Errors

The first cut in the field, and the vocabulary that hangs off it. You already made this cut correctly, unprompted. Here is its name, its source, and the machinery it determines.


The cut you already made

You were asked whether parse_config receiving a malformed file and parse_config receiving a null pointer were the same kind of failure. You said no: the first is something the user should be told how to fix, the second is a bug in the program.

That distinction is the organising principle of Joe Duffy's The Error Model (2016), the retrospective on Microsoft's Midori operating system and the most-cited survey of this design space. The two categories have names.

Recoverable error

A failure the programmer anticipated. Duffy: the result of "programmatic data validation" — "some code has examined the state of the world and deemed the situation unacceptable for progress."

Malformed config. Connection refused. File not found. Invalid credentials. Programs are expected to recover.

Bug

Duffy: "a kind of error the programmer didn't expect." Null dereference, index out of range, broken invariant, arithmetic overflow, unreachable branch reached.

Not recoverable — not because recovery is hard, but because it is undefined. See below.

The criterion is anticipation, and it is a property of your code, not of the universe. A disk failure is a recoverable error in a database that plans for it and a bug in one that does not. This is why the distinction cannot be read off a failure's cause — it has to be declared.

Why bugs cannot be recovered from

"Because the developer didn't expect this to happen, all bets are off. All data structures reachable by this code are now suspect." — Joe Duffy, The Error Model

This is the load-bearing argument, and it is worth being able to reproduce in a design review. A bug means one of your assumptions was false. You do not know which one. Any state the buggy code could reach may now violate its invariants, and code that runs afterwards — including your recovery handler, including your cleanup — runs against state you can no longer reason about. Catching the failure does not restore the assumption; it only hides that you lost it.

So the model prescribes the opposite of recovery. Duffy's term for it is abandonment:

"Abandonment unapologetically tore down the entire process in an instant, refusing to run any user code while doing so." — Joe Duffy, The Error Model

Note the two halves. Entire process — because the blast radius of a broken invariant is everything the process can touch. Refusing to run any user code — no finally, no destructors, no defer, no atexit hooks. Those are exactly the code that would run against corrupt state.

Term boundary

Abandonment is a designed, systematic response to a detected bug. Crash is what happens when nobody designed anything. They look the same from outside the process and are completely different engineering. When you say "it crashes" in a review, say which one you mean.

The general practice of doing this — failing immediately and visibly rather than limping on — is fail-fast, named and argued in Jim Shore's "Fail Fast" (IEEE Software, 2004). Shore's framing is about defect economics rather than state corruption: failing fast does not reduce the number of bugs, it reduces the distance between where a bug is introduced and where it is observed. Two independent arguments, same prescription — useful when only one of them lands with your audience.

Exercise 1 · Recognition

Find the misclassified failure

In each snippet, click the line that puts a failure on the wrong side of the cut. Not every snippet has one — if it is clean, say so.

Where each language draws the line

Every mainstream language has two mechanisms and a convention about which side each belongs to. The convention is almost never enforced by the compiler, which is why it has to be a review habit.

Language Recoverable error Bug
Rust Result<T, E>, propagated with ? panic!, unwrap, assert!, index out of bounds
Go error as a return value panic, nil deref, slice out of range
Python raise ValueError & friends assert, AssertionError, TypeError

Rust and Go make the split syntactic: a panic does not travel through the same channel as a Result or an error, so the two categories are visible in a signature. Python routes both through the exception mechanism, which is why except Exception: is dangerous in a way that Rust's match on a Result is not — it catches both categories with one clause.

Contested

Rust's panic is by default unwinding — it runs destructors on the way out and can be caught with catch_unwind. That is weaker than abandonment, which refuses to run user code at all. You can opt into the strict reading with panic = "abort" in Cargo.toml. Which default is correct is a live argument: unwinding lets a server isolate a bug to one request; abandonment says that request already touched shared state you can no longer trust. Both camps cite real production experience. The field has not settled this.

Contracts: how the cut gets declared

If "bug" means "unanticipated", then someone has to write down what was anticipated. That mechanism is older than Duffy and is called design by contract, from Bertrand Meyer's "Applying Design by Contract" (IEEE Computer, 1992). Three terms:

Precondition
What the caller must guarantee before the call. The list is non-empty. The pointer is non-null. The connection is open.
Postcondition
What the callee guarantees on return. The returned slice is sorted. The file handle is closed. The counter increased by exactly one.
Invariant
What holds of an object in every observable state, before and after every method. The balance equals the sum of the transactions.

The payoff is the blame rule, and it is the sharpest tool in this lesson:

A violated precondition is a bug in the caller. A violated postcondition or invariant is a bug in the callee. Either way it is a bug — which is why contract violations trigger abandonment, not an exception.

This gives an operational answer to "should this be an error or an assertion?" — a question that otherwise dissolves into taste. Ask instead: have I written this condition down as part of the interface? If the condition is in the contract, violating it is a bug and belongs on the abandonment side. If the function's job is precisely to determine whether the condition holds — a validator, a parser — then the failure is its normal output and belongs on the recoverable side.

Meyer's own argument for this is that contracts remove code: once responsibility is assigned, the redundant defensive check on both sides of the boundary is waste. Duffy reports the empirical counterpart from Midori — "90-something% of the typical uses of exceptions in .NET and Java became preconditions." That is, the great majority of what mainstream codebases throw as recoverable exceptions were, on inspection, contract violations wearing the wrong mechanism.

Exercise 2 · Execution

Assign the obligation

For a function withdraw(account, amount) on a bank Account, sort each clause into the right slot. Click a clause, then click a slot.

Exercise 3 · Recall

From memory

Write your answer before revealing. Getting it wrong and then seeing the answer builds more retention than reading the answer first — so do not skip ahead.

Glossary — added this lesson

Recoverable error — an anticipated failure the program is expected to handle. Duffy 2016
Bug — an unanticipated failure; renders all reachable state suspect. Duffy 2016
Abandonment — tearing down the process on a detected bug, running no user code on the way out. Duffy 2016
Fail-fast — failing immediately and visibly to shorten the distance between defect and symptom. Shore 2004
Unwinding — propagating a failure up the stack while running cleanup code (destructors, defer, finally). The weaker alternative to abandonment.
Precondition / postcondition / invariant — the caller's obligation, the callee's promise, the always-true property. Meyer 1992
Blame rule — precondition violated ⇒ caller's bug; postcondition or invariant violated ⇒ callee's bug. Meyer 1992

What this unlocks

You now have one axis. It answers which failures deserve handling at all — but says nothing about the shape of the handling for the ones that do. That is the next lesson: the three axes along which every error model varies (representation, propagation, handling), which is the frame that lets you compare Go's error, Rust's Result, and Python's exceptions on the same page instead of as tribal preferences.

← Status map Lesson 02 →

Sources: Duffy, The Error Model · Shore, Fail Fast · Meyer, Applying Design by Contract · RESOURCES.md