Support Ukraine

The Convergent Evolution of Exceptions

The eye, that magnificent organ for detecting electromagnetic radiation, has evolved a number of times[a] over the millions and billions of years.

The same thing seems to happen with exceptions.

While languages before Java have included exceptions, Java was the first language that forced programmers to deal with them in the form of "checked exceptions". Programmers hated it, and Java was the "peak exception" programming language. Ever since, they have been seen as something to avoid in programming language design, and the current set of contenders for the Next Big Language - Go, Rust and Zig - do not have them at all, instead relying on return values.

However, if Java was "peak exception", then this gang swung all the way to "peak return value". But exceptions make coding very pleasant because if your code is wrong, you crash. You don't have to worry about missing any errors - your code is either correct and running, or it has crashed. Not having to deal with "error handling" makes for uncluttered code because the vast majority of errors actually encountered cannot be handled. There are some classes of transient errors that can be handled, but in the vast majority of cases the error means that your assumptions about the external environment your code operates in are wrong and your best bet is to abort. For example, I have not seen any reliable pattern for "handling" an out of memory condition other than "free up memory by crashing and notify the user".

Now we're seeing some proto-exception features in Zig and Rust. Zig[b] goes the farthest by having an explicit try statement that works with the union types of the language so that if your function returns a type that is a union type with an error set, you can write try myFunction(...) and get the comfortable "if the function errors then just abort with the same error" behavior.

Rust[c] has the ? operator that unwraps a Result and early-exits on finding an error.

Go has neither and relies on the programmer remembering to check return values with if err != nil { ... }. Given that Go is very strict with comparatively unimportant bits like not letting you import any symbol that you don't use, while letting you opt out of noticing errors, I again return to the Go mascot: a cross-eyed gopher that is constantly running its little legs off while keeping its visual attention everywhere except in the direction it's actually going.

Of these, I think Zig will evolve try blocks first. After all, a try block is just a block where every function call is prefixed with try. For Rust it's slightly harder, since they chose an infix operator, but you could imagine a block where all function calls returning a Result are automatically unwrapped as if called with ?. Go seems to be sticking to its error result paradigm for now[d].

...and so the pendulum swings back, and all old is new again. Hopefully it will come to rest somewhere pre-Java, where you can use to "crash, don't thrash", but it doesn't turn into this invasive species that results in boilerplate code whose only purpose is to make the compiler stop complaining.