What’s wrong with exceptions? Nothing.

Mike Hearn
Mike’s blog
Published in
8 min readFeb 13, 2016

--

Lately it’s become fashionable to release new programming languages into the industry that have poor or non-existent support for exceptions. Sometimes, as with Go or Rust, this is sold as a feature rather than a bug. The result is that I’ve started to see some programmers argue in public forums that exceptions are a mistake and new languages shouldn’t have them.

I think that’s bad news. Exceptions are great, and I believe there’s a much simpler explanation for why they’ve gone AWOL in some newer languages — implementing them well is hard work for language designers, so some don’t bother and then choose to spin a weakness as a strength. Worse, some new languages are designed by people who spent their careers working in large C++ codebases that ban exceptions, so they probably don’t miss them.

In this article I’ll look at why exceptions are hard to implement well, why they’re so often banned in C++ and why we should demand them from our languages and runtimes anyway.

Image credit: Bea Tinerelli/ASDA

I’m pretty sure most developers already know this, but briefly, here’s a few reasons why exceptions are a good thing:

Code separation: Exceptions separate code for handling errors out from the main logic of your code. This not only makes it easier to read and review, but speeds things up at runtime too, because error handling code is also kept out of the CPU instruction cache. In modern computers where slow performance is often caused by cache misses, that’s useful.

Stack traces: Good runtimes give you stack traces with your exceptions, making it trivial to implement crash reporting. Crash reporters are vital in any complex piece of software. Adding a crash reporter to a desktop, mobile, web or even server app routinely reveals all kinds of odd failures occurring in the wild that you didn’t know about. Sometimes they are due to factors outside of your direct control, like buggy hardware.

For example, better crash reports are a competitive advantage for Android over iOS.

Failure recovery: Exceptions make it much easier to write apps that recover from failure. Many programmers will tell you that this isn’t possible, and the moment a programming error has occurred it’s game over and nothing less than a full app restart will suffice. Don’t listen to them. There are lots of real world apps that can and do survive errors caused by programming bugs, making them more robust than their market rivals.

As one example, IntelliJ IDEA can catch exceptions from plugins and very often carry on as if nothing had happened at all, except for (you got it) a crash report being submitted. For instance, if an exception is thrown in a static analysis due to a divide by zero, no problem — you just won’t get the results of that analysis. In servers, it’s typical to catch all requests at the scope of a single request, so a bug in a rarely used code path doesn’t risk taking down the whole site.

Finally, prototyping: when experimenting, you can easily skip error handling code entirely, without silently swallowing errors.

There are other nice benefits to exceptions, of course, but these are the ones that matter most to me. So why doesn’t every language have them?

I’m honestly pretty skeptical about some of the justifications quoted by the language designers in this area. They’re vague assertions that contradict my own experience — things like “exceptions make it hard to understand control flow”, which is not a problem I’ve ever encountered, but at any rate could be fixed with better rendering in IDEs for those who want it.

I think there’s a more obvious explanation — exceptions are expensive to implement properly, and the sort of people who are building these new languages just don’t value them much.

Why are exceptions hard work?

The most useful thing about an exception is the stack trace you get with it. Some time ago I did consulting work for a company that implemented its server side in Go. Whilst testing their new API product, I found a case that just returned a vague sounding error message. I notified them of this and expected it to be a simple fix — look in the logs for the stack trace of the internal fault, look at the code to see what was going wrong, fix. Done it loads of times.

Unfortunately it didn’t work out like that. Go does not have exceptions, so people use return codes instead. There wasn’t any good way to find out where in the codebase the errant code was, because return codes don’t track the origins of errors and this one was a general code. And the error didn’t occur in the dev environment, so attaching a debugger to the live server wasn’t going to be possible. And Go online debuggers didn’t work very well anyway, they said. Doh. A stack trace would have been super nice.

Generating accurate stack traces is harder than it looks. Optimising compilers love to inline functions into each other. Function inlining is a powerful optimisation because once the code of a function has been copied into another, the now larger function can be optimised as a whole, often unlocking yet more optimisations that weren’t previously possible before the inlining was done. But if your functions have all been splatted together, how do you retrace the steps the program took to reach a throw site?

Scripting language runtimes usually don’t have this problem because they don’t aggressively optimise to start with: people just accept that interpreted dynamic languages like Ruby will be slow. Cutting edge runtimes like the JVM or .NET CLR want performance and convenience, so their just-in-time compilers are specifically designed to generate lots of metadata along with the machine code. The metadata allows the stack trace generator to unpick inlining and other optimisations that could corrupt stack traces.

Generating and then mapping the real machine stack back through all the metadata tables is complex and time consuming, so state of the art JIT compilers like HotSpot will do things like recompile methods on the fly to disable stack trace generation, if profiling shows that your code is throwing and catching exceptions way more often than expected (for instance, as part of normal operation).

But doing this all this stuff requires complex support in both the compiler and runtime, and as a result it’s very tempting to skip it. The trend towards using LLVM as a backend doesn’t help because LLVM was designed to compile C++ and C++ exceptions do not capture stack traces.

Actually, C++ exceptions are worse than useless: many large C++ codebases have style guides that forbid them entirely! Given the career backgrounds of the people who are building languages like Rust, Go, Swift etc, it’s probably not surprising that they don’t value exceptions much considering that for big chunks of their life they were not able to use them.

Why are C++ exceptions so useless?

The main reason C++ exceptions are so often forbidden is that it’s very hard to write exception safe C++ code. Exception safety is not a term you hear very often, but basically means code that doesn’t screw itself up too badly if the stack is unwound. This bit of Java code is exception safe:

public static List<SomeComplexObject> list = new ArrayList<>();// ... some time later ...SomeComplexObject foo = new SomeComplexObject();
foo.loadFrom("http://bar.com/whatever");
list.add(foo);

If the loadFrom method throws an exception, the global list is untouched. This code isn’t exception safe:

SomeComplexObject foo = new SomeComplexObject();
list.add(foo);
foo.loadFrom("http://bar.com/whatever");

We’re adding the object to the list before it’s been fully initialised. If there’s a problem retrieving that URL, we might end up with a half baked object exposed to other bits of the program. That’s especially bad in multi-threaded apps. Oops.

In C++ you could write the above code in a few different ways. Here’s a naive translation from Java:

std::vector<SomeComplexObject*> list;

// some time later

SomeComplexObject *foo = new SomeComplexObject();
foo->loadFrom("http://...");
list.push_back(foo);

It’s buggy: if loadFrom throws an exception then we will leak memory, as there is no garbage collector to help us clean up. If this happens once it’s probably survivable. If it keeps happening, we can eventually exhaust our heap and crash.

The above is not what is often called “modern C++”. Let’s try again:

std::vector<SomeComplexObject> list;

// some time later
SomeComplexObject foo;
foo.loadFrom("http://...");
list.push_back(foo);

The syntax is different now. The vector (list) doesn’t contain pointers anymore, now it contains full instances of SomeComplexObject next to each other in memory. We construct our new object, which is allocated on the stack, and call loadFrom. This fixes the leak because if an exception is thrown, “foo” will be automatically deleted as the stack unwinds. But now we have a new problem, which is that push_back() will copy the object into its final resting place! That’s a problem because:

  • Copying a big complex object can be slow. We’re probably writing C++ because we want things to be fast.
  • The same style guides that forbid exceptions often ask that you block copy constructors by default unless you’ve specifically designed for copying, as otherwise it can cause other kinds of bugs. The Google style guide does this.

Worse, a common way in C++ to add things to a map is to request a non-existent key, which then constructs the class in place with a default constructor. This avoids copying, but means if anything goes wrong during post-construction setup you’re back to having a half-baked object exposed to other code.

Newer versions of the C++ language introduced move constructors which help with this type of problem, but it’s too recent to help people who have spent years programming in environments where writing exception safe code was incredibly awkward … and thus forbidden.

Conclusion

We shouldn’t be surprised that faced with the high cost of providing performant exceptions with detailed stack traces, language designers who spent years working in C++ will be tempted to either skip some of their features, or skip exceptions entirely … whilst arguing that they’re “bad practice” and thus you shouldn’t miss them.

If you genuinely find exceptions confusing, then by all means, code without them. Just be aware that return code oriented programming can lead to numerous and subtle bugs that can be far more painful for whoever ends up maintaining your code. Exceptions became popular for a reason.

Read this entertaining chapter by the excellent Raymond Chen about making DOS games run on Windows 95 to see what havoc buggy apps that accidentally ignored or misinterpreted error codes could cause. And for those of us who actually remember DOS extenders, XMS pages and other obsolete trivia from the 1990’s, let’s take a moment to appreciate how much progress programming has made in the last twenty years.

--

--