The State of Exceptions in C++

Thomas Hansen
throws std::out_of_bounds(“This error type may have reached the end of its usable range”);

Exceptions work through a relatively clever mechanism, making them unique among the languages that include them. Instead of just modifying a variable or switching between two addresses in memory, they “unwind the stack”, propagating the error upward until it is handled or the program terminates.

Each function stores its variables and state information on the stack, typically starting at a higher memory address and allocating each function on a lower address. Once an exception is thrown, it escalates upward through each frame pointer on the stack, cleaning up resources through RAII and dismantling stack frames. Eventually, the exception is caught, or if unhandled it reaches the main function and triggers the terminate() syscall. Below is an example showing how this works:


#include 
#include 

void foo() {
int a{0}; // memory address on stack
printf("%p\n", &a);

throw std::runtime_error("Our exception");
}

int main() {
int a{0}; // memory address on stack
printf("%p\n", &a);

foo();

printf("Never prints\n");
}
                

0x7ffcdc530734
0x7ffcdc530704
terminate called after throwing an instance of 'std::runtime_error'
what():  Our exception
Aborted (core dumped)
                

This mechanism has many advantages. The call stack can help in debugging, allowing the developer to see which functions are involved in the call stack with an error, and it makes error handling simpler for the developer of the library to incorporate.

In a tour of C++20 on page 45, the creator of C++ Bjarne Stroustrup encouraged their use, writing:

"Use of the exception-handling mechanisms can make error handling simpler, more systematic, and more readable"

So they are an interesting feature, which add an interesting and new paradigm to the library that many people like. So why write this article?

Dissent

We can start by looking at the Google C++ coding guide and best practices. While it’s not without its controversy, Google is big enough and the guide is popular enough that it can’t be ignored. In it, they succinctly state:

"We do not use C++ exceptions."

And go on to describe situations where they produce dangerous code. In particular, since exceptions can easily take down your program, they must be caught and taken seriously. When third party libraries add exceptions, it can be easy to not update the code properly, and they may crash your program in ways you couldn’t appropriately plan for.

Modern system languages have taken a stance against exceptions. Golang, Zig, and Rust do not incorporate exceptions at all. Languages who have built their reputation on fault tolerance like Elixir have refused to add them as well, relying on strict error handling instead.

Even Stroustrup, who’s not opposed to exceptions, has been implementing alternatives into C++. C++17 saw the introduction of the optional<T> type, making it easier to let the caller know a certain return value doesn’t exist. In C++23, we’re seeing the introduction of std::expected<T, E>, which allows for a return type and an error type, similar to what we see in many other languages like Rust and Swift, and intellectually similar to what we see in Go.

So why the move away from exceptions? I think it has to do with shifting needs in industry. Although exceptions are nice to use while building libraries, they make it easier to produce code which could fail unexpectedly. In deployed, containerized environments can mitigate this risk, but if you’re running on modern hardware in a fault tolerant system it often makes more sense to use a faster development language like python. If your Django site or FastAPI instance encounters a critical issue it may be better to “fail fast” and restart the container, instead of attempting to recover. If you’re building your software in C++ you’re possibly in an environment where there isn’t so much overhead; either it’s an embedded device, or performance is critical, and you may not have the overhead to containerize your workflow or restart a device running in the field.

In my personal experience I’ve done a lot of work on satellites, and restarting a satellite due to an exception could mean missing a communication pass or the satellite tumbling, both of which put you on a course to losing a satellite and the years of work that went into it. This risk is unacceptable, which is why C++ based frameworks like NASA F` (F Prime) explicitly ban exceptions.

Outside of high reliability systems, opinions may differ. A U of New Mexico professor I spoke with referred to exceptions as “the hot new thing”, but in his professional experience didn’t involve building satellites. He taught a class on Java and did research, both of which have a high tolerance for runtime errors.

While there’s always a discussion to be had, I think the use of exceptions in modern C++ is limited. The risk of a new library update creating bugs or a developer poorly implementing exception catching creates an inherent risk that most projects shouldn’t have to tolerate. While projects which can effectively mitigate the safety risks surely exist, much like the goto I don’t believe exceptions should exist in most modern C++ applications.