The Error of Exceptions
Why I think exceptions are bad language design.
When my classmates and I first learned about exceptions (in C++), I thought “Cool, I don’t have to return -1
and nullptr
any more”. But a while later a friend brought up a good question: What’s the difference between exceptions and goto
s?
The Problem of Exceptions
Look at the following Java code and tell me what it could return:
On the first look, this method could return a String
, null
or throw a NullPointerException
, if getCurrentUser
returns null
.
But that’s not all, it could also throw any other unchecked exception. Maybe getCurrentUser
returns an IllegalStateException
, because some initialization is missing?
For the username it might be harmless, but what about the following?
This kind of code can easily sneak through code reviews. But what if any one of the cache unloads throws an exception? In that case all of the other caches wouldn’t be unloaded and if Cache
is an interface, it isn’t even clear what exceptions could be thrown.
This mistake is so common that many languages nowadays have a version of the try-with-resourses
statement. Usually, they are used for file streams or connections to ensure that they are closed properly. But it’s easy to overlook for other cases, where it’s fine most of the time. Be honest: how often do you think about “What if this method throws an exception?”
Exceptions are worse than gotos (in some ways)
Back to the original story. I don’t know what answer we came up with back then, but this is what I’d say now:
They are similar, but there are a few differences:
- I can send some data with it,
- I can contain it when calling a method (by wrapping it in a
try-catch
), and - I don’t know where I’ll be going.
The last one is a pretty big deal: When you see a throw
statement, you don’t immediately know, what code will be executed next. Worse even, sometimes you can’t know, because the method could be called from multiple sources.
When writing a method this is perfectly fine — the method does one thing and that’s all we should care about. We should ignore the context around it for the most part anyway.
But when reviewing code, this can quickly become hard to understand/read. And how would I make sure that all callers of my method properly handle it?
Which leads to the problem of the previous example: How do I know what exceptions a method can return? And the answer is, that I’d have to read all of the code that that method might use and look for throw
statements. Then I need to see if they’re handled or not. Even some libraries with otherwise good documentation like to keep an air of mystery around which exceptions are thrown and when.
Exceptions are better than gotos (in some ways)
So why are exceptions then commonly accepted, while goto
s are almost universally avoided?
The answer is the first two points in the previous section. If used correctly, exceptions aren’t that bad.
You can catch exceptions
Unlike goto
s, the caller of a function can actually interact with exceptions. You can ensure that even if an exception is thrown, you get a chance to close a file stream, close a connection or still execute some code to prevent an illegal state.
This already mitigates a big part of the danger of goto
s as I can make sure that certain code is executed in any event.
You can send some data with it
Sending additional information about why this jump outside of the regular flow is made can give considerate context to it. Instead of just telling you which code should be executed next, it tells you what happened.
Of course, this can be abused. Exceptions can now also be used as an additional return value of functions — and sadly, often are. There is this idea to not use exceptions for flow control, but all exceptions necessarily control the flow of a program. You just need to make sure that an exception is actually the exception.
The alternative
So what should we’ve learned instead to replace all of the -1
s and nullptr
s?
One of my favorite things to make an API safer and easier to use is extending its domain. What I mean by that is that it takes any error states and handles them the same way that it would success returns: It accepts it as valid input and presents it to the user, in the same way, encouraging and sometimes forcing a user to gracefully handle them both.
This can be done generically with the Result
type (also sometimes called Throw
or Either
). A very basic implementation is below:
The implementation is very basic and if you want to use it, you’d probably want to add more utility functions, but the idea is there: The user gets a Result
instance, so he already knows that it could either have a success state or a failure state, and as such, he cannot forget about either of them.
Ideally, you wouldn’t use a generic Throwable
but actual objects that are tailored to the different errors that can appear in any given function. In a way, Optional
(also often called Maybe
in other languages) are a special form of Result
.
This approach has many advantages:
- Users cannot forget about errors,
- the api can explicitly list all possible errors and their values without bloating the function itself or relying on documentation and
- it is more apparent where failures are handled.
This monadic way of handling exceptional states can already be found in many newer apis: Linq in C#, Streams in Java, Promises in JavaScript and many more
So, should you replace all exceptions with results?
If we want to wrap a function that potentially throws errors with the result type, it would look like this:
That’s quite a bit of boilerplate code that would have to be repeated for almost every third party function and functions from the standard library. Sure, you can create a generic wrapping function, but is it really worth it?
Honestly, for most cases I don’t think so. I try to avoid creating new exceptions whenever possible, but I wouldn’t go through the effort of wrapping already existing functions just so I can avoid them. If anything, this should’ve been addressed by the language itself.
Not every exception is worth caring about
Code like b/c
might throw an ArithmeticException
if c
is 0
. But we need to address this only if we didn’t first check that this cannot be the case. With exceptions we can ignore it by just simply doing nothing. Most likely we’ll eventually fall into a catch-all that logs the issue and tells the programmer to check this.
With the Result
type, we have to explicitly ignore it, so we’re automatically prompted to check whether that’s possible. This might result in some additional unnecessary code, but it makes it just a bit more resilient.
Summary
If we’d learned about Result
types rather than exceptions, I believe, our reaction would’ve largely been the same. They might even be slightly easier to learn and as such would’ve been introduced earlier.
That doesn’t mean that exceptions are actually as bad as goto
s. But they are frighteningly close for how common they are. And I’ve definitely wasted hours before when trying to figure out what exceptions a library method might throw. And of course I’ve also overlooked some of them before.
Maybe even worse, exceptions can easily be misused as a way to return multiple different return values. And sadly, I’ve seen this done before.
Personally, I wish that more languages would adopt this more “functional” style of error handling instead of exceptions (like rust already did). I believe that many errors can be prevented in this way.
Resources
If you want to play with my cache example and Return
a bit, you can do so on this online-java doodle.
If you want to play around with my beautiful mermaid diagram, you can press this link.