Lecture · 7 slides
Errors: exceptions, values, and what each hides
Error handling is the part of a program that everybody writes last, tests least, and argues about most. It is also, by a wide margin, where production incidents come from — not because the failure was unforeseeable, but because the code that was supposed to cope with it had never once been executed.
Underneath the arguing there is a genuine design question with two coherent answers. Either failure is a special channel that bypasses the ordinary flow of the program, or failure is an ordinary value that the ordinary flow carries. Exceptions are the first. Result types are the second. Everything else — error codes, callbacks with an error argument, nullable returns — is a variation on one or the other.
Both answers are defensible, both are in wide production use, and the choice between them is a trade rather than a discovery. This lecture is about what each one makes easy, what each one hides, and about the small number of things that are true regardless of which you pick.
AssumesYou have written a try block, and have at some point caught an exception and not known what to do with it.
Slide 1
What exceptions actually do
An exception removes failure from the flow of the code entirely, so that the main path can be written as though nothing goes wrong.
data = fetch(url) rows = parse(data) save(rows)python Three lines, each of which can fail, and no error handling in sight. If
fetchthrows, the remaining lines simply do not run and control jumps to whatever handler is above. The happy path reads as a description of the intent, which is genuinely valuable — this is far easier to follow than the same logic with three failure checks interleaved.The cost is the exact mirror image. Nothing in those three lines tells you which can fail, what they throw, or where it will be caught. Every line is a potential exit from the function, invisibly, and reasoning about what state things are left in requires knowing something the code does not say.
Slide 2
What returning errors does
The other approach makes failure an ordinary value, which means the type system can see it and the reader cannot miss it.
let data = fetch(url)?; let rows = parse(data)?;rust The
?is doing what the exception did — returning early on failure — but the difference is that it is written down.fetchreturns aResult, its signature says so, and there is no way to use the value without acknowledging that it might not be there. Forgetting is not a runtime surprise but a compile error.The price is noise, and it is not nothing. Every fallible call is marked, every signature is wider, and code that does ten things in sequence carries ten annotations. Go's version, without the
?, famously spends three lines on every call. Whether that is honesty or ceremony is exactly what people disagree about, and both sides are describing the same characters on screen.Slide 3
Checked exceptions, and what went wrong
Java tried to have both, and the experiment is worth understanding because its failure is usually misdiagnosed.
Checked exceptions put the failure in the signature — a method declares what it throws and callers must handle or declare it — which is precisely the guarantee a Result type gives. In principle it is the best of both. In practice it produced a generation of code containing empty catch blocks, and the feature is widely considered a mistake by people who otherwise like the language.
The reason is instructive. The rule was all-or-nothing: every checked exception had to be handled somewhere, immediately, even when the honest answer was "this cannot happen here, let it propagate". Combined with interfaces that could not add throws clauses later, the pressure was overwhelming to write the shortest thing that compiled, and the shortest thing that compiled was a catch block that did nothing. The idea was sound and the ergonomics defeated it, which is a lesson about language design more than about errors.
Slide 4
Not everything is the same kind of wrong
The distinction that resolves most of the argument is not between mechanisms. It is between two kinds of failure that people insist on treating alike.
Expected failures are part of the problem: the file is missing, the input is malformed, the network timed out, the user typed nonsense. These will happen, the caller can often do something sensible, and they belong in the signature where the caller can see them.
Bugs are different. An index out of range, a null where there cannot be one, an invariant violated — these mean the program's model of itself is wrong, and there is usually nothing to do but stop before more damage is done. Rust draws this line in the language:
Resultfor the first,panic!for the second. Most languages leave it to convention, and most codebases are worse for it — wrapping a null dereference in a retry loop is treating a bug as weather.Slide 5
The failure nobody writes
Whichever mechanism you choose, the commonest bug in error handling is the handler that swallows.
try { save(rows); } catch (e) { console.log(e); }javascript The program continues as though the save succeeded. Something later reads data that is not there, fails in a way that has nothing to do with the cause, and somebody spends a morning on it. This is worse than not catching at all, because it converts a loud failure into a quiet corruption.
The rule worth adopting is that catching an error obliges you to do one of three things: fix it, translate it into something the caller can act on, or let it continue upward. Logging it is not handling it. And an empty catch block is a claim that nothing can go wrong here, which should be written down, because it is almost always wrong.
Slide 6
Where the error is caught matters more
The mechanism gets the attention; the placement is what actually decides whether a system is robust.
An error should be handled where there is enough context to decide what to do. A function three levels down knows a write failed and has no idea whether to retry, abandon the operation or alert somebody. The request handler knows. So most code should not handle errors at all — it should let them travel to the small number of places that can make a decision, which are usually the boundaries of the system.
This is why the pattern that hangs in the Hall of Composition as Chain of Responsibility keeps reappearing in error handling: a sequence of handlers, each deciding whether this one is theirs. And it is why "handle errors early" is bad advice while "validate input early" is good advice. They sound alike and they are opposites.
Slide 7
What both sides agree on
For all the argument, the points of agreement are larger than the dispute, and they are the ones worth taking away.
Failures should be visible in the signature where the language allows it, because a caller cannot handle what it does not know about. Errors should carry enough information to act on — a message a human can read and a cause that has not been discarded. The distinction between an expected failure and a bug should be real in your codebase, whatever your language calls it. And error paths should be tested, because an untested handler is code that has never run and will first run during an incident.
What genuinely remains open is the ergonomic question: whether the noise of explicit errors costs more than the invisibility of exceptions. Rust, Go and Haskell bet one way. Java, Python and C# bet the other. Both bets have produced systems that run for decades, which is the strongest evidence available that this is a trade rather than a mistake — and a reason to be suspicious of anyone who tells you their side settled it.
Works in the collection
The arguments above are hanging on the walls of the museum, in one form or another. These are the ones worth looking at next.
The rest of the programme
- Compiled, interpreted, and the space betweenA distinction that stopped describing anything decades ago, why it persists, and what is actually different about the machinery underneath.
- What a compiler actually doesFour jobs in a row, each mechanical, none magic: text to tokens, tokens to a tree, a tree to a judgement, and a judgement to instructions.
- Functional programming and object orientationTwo ways of arranging a program, what each one genuinely makes easy, and the trade-off underneath the argument that neither side can escape.
- The principles of object orientationEncapsulation, inheritance, polymorphism and SOLID, one at a time: what each actually claims, which held up, and which its own community abandoned.
- Types: what they can and cannot proveA type checker proves one thing about every possible run of your program. Knowing which proposition explains both the enthusiasm and the disappointment.
- Null, and the mistake its inventor apologised forTony Hoare called it his billion-dollar mistake. What was actually wrong with it, what the alternatives cost, and why the fix took forty years to arrive.
- What abstraction costsAbstraction is sold as free and is not. What you buy, what you pay, and how to tell before writing it which of the two is larger.
- Immutability, and what it is not free ofValues that never change buy sharing, comparison and time travel. They are not free, and it is worth knowing where they are expensive before you commit.
- Recursion, and why it feels like cheatingA function that calls itself looks like an unpaid debt. What makes it terminate, what it costs on the stack, and why some problems resist any other shape.
- Big-O, and what it deliberately ignoresComplexity notation throws away constants, hardware and every input you will actually see. Knowing what it discards is what makes the number useful.
- What regular expressions cannot matchThere is a precise boundary around what a regex can recognise. It explains the famous refusal to parse HTML, and why some patterns run forever.
- Concurrency is not parallelismOne is a way of structuring a program, the other a way of executing it. Keeping them apart explains why async helps a web server and threads often do not.
- Why programs are hard to changeSoftware is called soft because it can be edited. Why editing gets harder every year, what the mechanism is, and which of the usual remedies work.