I don't think error handling is a solved problem in language design

March 17, 2025

There are certain things about programming language design that are more or less solved problems, where we generally know what the good and bad approaches are. For example, over time we've wound up agreeing on various common control structures like for and while loops, if statements, and multi-option switch/case/etc statements. The syntax may vary (sometimes very much, as for example in Lisp), but the approach is more or less the same because we've come up with good approaches.

I don't believe this is the case with handling errors. One way to see this is to look at the wide variety of approaches and patterns that languages today take to error handling. There is at least 'errors as exceptions' (for example, Python), 'errors as values' (Go and C), and 'errors instead of results and you have to check' combined with 'if errors happen, panic' (both Rust). Even in Rust there are multiple idioms for dealing with errors; some Rust code will explicitly check its Result types, while other Rust code sprinkles '?' around and accepts that if the program sails off the happy path, it simply dies.

Update: I got Rust's error handling wrong, as pointed out in the comments on this entry. What I was thinking of is Rust's .unwrap() and .expect(), not '?'.

If you were creating a new programming language from scratch, there's no clear agreed answer to what error handling approach you should pick, not the way we have more or less agreed on how for, while, and so on should work. You'd be left to evaluate trade offs in language design and language ergonomics and to make (and justify) your choices, and there probably would always be people who think you should have chosen differently. The same is true of changing or evolving existing languages, where there's no generally agreed on 'good error handling' to move toward.

(The obvious corollary of this is that there's no generally agreed on keywords or other syntax for error handling, the way 'for' and 'while' are widely accepted as keywords as well as concepts. The closest we've come is that some forms of error handling have generally accepted keywords, such as try/catch for exception handling.)

I like to think that this will change at some point in the future. Surely there actually is a good pattern for error handling out there and at some point we will find it (if it hasn't already been found) and then converge on it, as we've converged on programming language things before. But I feel it's clear that we're not there yet today.


Comments on this page:

Ah, Lisp. At a first glance, signals and restarts might look like exception handling, but the signal handler can send control back into any function that has provided an appropriate restart. The stack frames (and local variables) aren’t gone until the handler has made a choice of destination. It’s closer to having a list of named setjmp points to pick from than the familiar try/catch idiom.

By Anonymous at 2025-03-18 08:55:08:

Just curious, but: I wonder how functional languages like Haskell do error handling (and if that differs from or conforms to the examples you have given in your post). (I have no personal experience here).

From 169.150.198.91 at 2025-03-18 09:32:29:

In Haskell the best practice is to use the Maybe/Either types that are equivalent to the Option/Result types in Rust, but it also has exceptions and uses them in its standard library. An example is "head" which returns the first element of a list, or throws an exception if the list is empty. There are also libraries that provide safe versions of the standard library functionality without using exceptions, like safe.

Hi Chris,

apologies for the long stream-of-consciousness answer (read "rambling"). Have mainly used these concepts in decades of programming practice working an actual problems. The theory and terminology comes from tying all the loose ends together.

On a deep level, error handling isn't about programming language design at all. It's about the problem domain. And that's scary.

Here is a rough working hypothesis as to why it's so hard.

Firstly, for-loops and conditionals ("if"s) seem to be fundamentally different. For-loops deal with doing the same thing over and over. So they are closely tied to symmetries in problem domains. If it cannot be treated the exact same, then it fails to be symmetrical. Symmetry is most deeply studied in (mathematical) Algebra.

Secondly, conditionals ("if"s) deal with doing things differently, depending on some condition. So they are closely tied to asymmetries in problem domains. If it has to be treated differently, then it's asymmetrical. For example, two cities A and B can be close to one another and far away from a third city C. Asymmetry is most deeply studied in (mathematical) Geometry.

Thirdly, the farther you go in mathematics, the less Algebra and Geometry get along. Alexander Grothendieck ran into this problem while thinking through Functional Analysis (if you want to save time, then be advised, that this rabbit hole strays far away from programming, at times). Starting with nices symmetries (Algebra) and taking them to their logical conclusion yields hairy asymmetries (Geometry). And, separately, starting with asymmetries (Geometry) and taking them to their logical conclusion (something mostly done with Algebra, curiously) yields hairy symmetres (Algebra). They just don't seem to mix at all, and the longer you look the worse it gets.

Forthly (and back on planet earth), this means that for-loops and "if"s don't get along at all. And the more intricately they are used, the less they get along. And the specific programming language doesn't even make a difference. (Computers "only" provide constructive proofs for electronic data processing problems, after all.)

[See below, for the awkward and very specific term "Universal Programming Language", "if"s and for-loops.]

Fifthly, the world around us seems to be Geometric in nature rather than Algebraic. In programming this means that "if"s are much more important and intricate than for-loops.

Sixthly, error handling pertains to the "if"s of edge cases and things going wrong in the problem domain. There are no short cuts. There are no fancy loops or symmetries to ever make this easier by orders of magnitude. The problem domain is what it is. And pushing this onto programming languages is the kind of wishful thinking that providies something that only resembles hope.

Kladov (2023) talks about one arrangement "if"s and for-loops. The article is titled "Push Ifs Up and Fors Down". Nevertheless, it seems like "Pull Ifs Up and Push Fors Down" may have been a little easier to remember. And of course structure-of-arrays vs. arrays-of-structures. And Fortran vectors.

For cascading failures Adam (2017) has a very theoretical approach to system fundamentals. (IIRC, one of his examples is about cascading failures in electrical grids and overloaded switching stations. If you have enough experince as a sys-admin, this kind of problem may not be new.)

Apologies for there being seemingly no good answer.







For the theory-minded, here is a big pile of stuff to consider:

Waldo et al. (1994) indicates that distributed systems are a combination of (a) partial failure and (b) concurrent execution. This is important here, because of failures and partial failures. Concurrent execution ties into multi-programming a.k.a. multi-processing, see also retentrant functions. This is in contrast to multi-threading and Green-threads a.k.a. async functions a.k.a. fibers.

Adam (2017) dives into (a) injectivity and (b) exact sequences avoiding generative effects. In this context, generative effects are cascading failures. You can also have those in power systems. A few nicer pictures around this may be found in Fong and Spivak (2018) chapter one and the other chapters. Chapter one mentions Adam (2017) somewhere.

Asymmetry seems to be the core of (a) ACID SQL transactions, (b) General Relativity in Physics, (c) distributed systems (discrete time) clock problems like Lamport clocks Lamport (1978), and (d) the CAP Theorem. All these problems deal with having transactions, consistent state, and "the same points in time". Come to think of it, software versioning and dependency resolution might also be adjacent to this hairy ball of ideas.

There also seem to be trade-offs as to how much big-oh space complexity can be paid to reduce big-oh time-complexity (sorry, didn't save a reference on this one. It was on HN within the last two or three weeks, IIRC).

Incomputability of Kolmogorov Complexity also ties into this. Kolmogorov complexity is about "the best for-loop" to describe a data set. See also: Halting Problem and Busy Beaver Number.

And P vs. NP also seems to be about solving compuational problems in a mostly Geometric world.

Dan Luu (a) tears into Brooks's differentiation of accidental and essential complexity as applied to programming practice with real electronic computer systems and (b) mentions somewhere that the most impressive programs he has ever seen were written by people with deep knowledge of the problem domain.







Links, References and Further Reading

Waldo et al. (1994)
A Note on Distributed Computing
https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf

Adam (2017)
Systems, generativity and interactional effects
https://dspace.mit.edu/handle/1721.1/109012

Fong and Spivak (2018)
Seven Sketches in Compositionality: An Invitation to Applied Category Theory
https://arxiv.org/pdf/1803.05316

Lamport (1978) Time, Clocks, and the Ordering of Events in a Distributed System
https://lamport.azurewebsites.net/pubs/time-clocks.pdf
https://en.wikipedia.org/wiki/Lamport_timestamp

CAP Theorem
Gilbert and Lynch (2002)
Brewer's conjecture and the feasibility of consistent, available, partition-tolerant web services
https://dl.acm.org/doi/10.1145/564585.564601
https://en.wikipedia.org/wiki/CAP_theorem

ACID Property of Database Transactions
https://en.wikipedia.org/wiki/ACID

General Relativity
Einstein (1916)
Relativity: The Special and the General Theory
https://en.wikipedia.org/wiki/General_relativity

Brooks (1975, 1982, 1995)
The Mythical Man-Month
https://en.wikipedia.org/wiki/The_Mythical_Man-Month

Luu (2020)
Against essential and accidental complexity
https://danluu.com/essential-complexity/

Luu (2015)
A defense of boring languages
https://danluu.com/boring-languages/
Importat quote: "[...] my experience has been that the things I've learned mostly let me crank through boilerplate more efficiently. While that's pretty great when I have a boilerplate-constrained problem, when I have a hard problem, I spend so little time on that kind of stuff that the skills I learned from writing a wide variety of languages don't really help me; instead, what helps me is having domain knowledge that gives me a good lever with which I can solve the hard problem. This explains something I'd wondered about when I finished grad school and arrived in the real world: why is it that the programmers who build the systems I find most impressive typically have deep domain knowledge rather than interesting language knowledge?"

Kladov (2023)
Push Ifs Up And Fors Down
https://matklad.github.io/2023/11/15/push-ifs-up-and-fors-down.html

Kolmogorov Complexity
Kolmogorov (1963)
On Tables of Random Numbers
https://en.wikipedia.org/wiki/Kolmogorov_complexity

Halting Problem
Church (1936)
An Unsolvable Problem of Elementary Number Theory
https://en.wikipedia.org/wiki/Halting_problem

Busy Beaver Number
Radó (1962)
On non-computable functions
https://computation4cognitivescientists.weebly.com/uploads/6/2/8/3/6283774/rado-on_non-computable_functions.pdf
https://en.wikipedia.org/wiki/Busy_beaver

P vs. NP
P versus NP problem
https://en.wikipedia.org/wiki/P_versus_NP_problem




Term: "Universal Programming Language"

Definition: A universal programming language offers the following three features:
(UL1) Sequences of Instructions (Semicolons and colons in C89)

(UL2) Conditionals (if and ?: in C89)

(UL3) Iteration a.k.a. Repetition (while, do-while, for, and goto in C89)

Note 1: For everyday programming this concept is a very good proxy for Turing Completeness. Notable example: Sieve script (IETF RFC 3028 by Tim Showalter) for email filtering lacks (UL3) and this implies it lacks Turing Completeness and, therefore the Halting Problem doesn't apply.

Note 2: This term is a very specific term from theoretical Computer Science. It is notoriously difficult to google for. Have seen it used once at a conference and once in a lecture by different CS professors. The tern seems to be older.

Links: https://en.wikipedia.org/wiki/Turing_completeness https://www.studocu.com/en-gb/messages/question/2906373/what-is-a-universal-programming-language https://learnprogramming.academy/programming/sequence-selection-and-iteration-the-building-blocks-of-programming-languages/

By herbertmoroni at 2025-03-19 12:31:01:

Great article! I think Node.js and C# offer two more contrasting examples that further strengthen your point about the lack of consensus in error handling.

Node.js embraces a hybrid approach with both callbacks and promises. In callback-style Node, errors are typically the first parameter in callback functions (error-first callbacks), creating deeply nested error handling that led to "callback hell." This was later addressed with Promises and async/await, which moved closer to exception-style handling while maintaining the possibility of explicit error checks.

Meanwhile, C# takes yet another approach with its structured exception handling and the more recent pattern matching techniques. While it primarily uses exceptions, C# has evolved to include Result/Maybe-like patterns through nullable reference types and the modern pattern matching syntax for more explicit error handling without abandoning exceptions.

Joe Duffy has a long assay about all the avenues of error handling patterns they explored while working on the Midori research OS at Microsoft: https://joeduffyblog.com/2016/02/07/the-error-model/

By judson at 2025-03-20 15:51:20:

not the way we have more or less agreed on how for, while, and so on should work.

I'm not sure that we've really "agreed" to the extent you imply, which leads me to disagree with your conclusion/hope.

A lot of languages inherit from C, and so copy the semantics of "for" and "while"—just as they copy braces. "do{}while" seems unpopular by comparison. "goto" is contentious, with labelled "break"/"continue" statements a common alternative.

It's common for languages to differ even in loop design. Python is a good example: it has "for x in y", where "y" is some type of "iterable", making this a type of "for-each" loop. There's no 3-part C-like "for"; people who want such a thing can use the "range" function, which ergonomically produces an iterator to be passed to the for-each.

I like to think that this will change at some point in the future.

So, this is where I'm sceptical. What you're saying is essentially that humanity will decide which of several incompatible philosophies is correct.

The Zig language specifically advertises "no hidden control flow", because the designer sees exceptions as problematic (also: operator overloading, and function calls disguised as attribute accesses). Other designers see exceptions as a more ergonomic way to handle errors, compared to passing return values around. Sometimes, an exception-like feature is provided for general control flow even in non-exceptional circumstances, as in some Lisps.

A related problem, that may actually be solved someday, is how to define an error hierarchy. A common pattern among Unix C programmers is to return some value from errno.h, that's vaguely like what they're describing; like returning EEXIST "File exists" when trying to use a hash table key that's already in use (others might use EALREADY "Operation already in progress" or EBUSY "Device or resource busy"). Even a value with a good matching description could benefit from some type of subclassing, like EPERM "Operation not permitted"... for the reason that the thread trying to unlock the mutex isn't the one that locked it. Exception hierarchies tend to make such things easy, but enumerations often don't, and there are ways to solve that (all with complex trade-offs, of course).

By erenon at 2025-04-01 03:43:46:

other Rust code sprinkles '?' around and accepts that if the program sails off the happy path, it simply dies.

It seems to me that this sentence confuses `?` (that propagates the error up to the caller) with `.unwrap()` (that dies, if the unwrapped value is Err).

By judson at 2025-03-20 15:51:20: A related problem, that may actually be solved someday, is how to define an error hierarchy.

Even with a proper hierarchy, "FileNotFound" is not useful to me. "foo/bar/baz.c was not found" (or even, couldn't be opened), is much more helpful. Errors need dynamic context, that is sometimes costly.

Written on 17 March 2025.
« OIDC claim scopes and their interactions with OIDC token authentication
How ZFS knows and tracks the space usage of datasets »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Mon Mar 17 22:53:22 2025
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.