C++ exceptions are a powerful and elegant way to handle “things that should not happen” in your code; and yet after some 30 years from their introduction, they are considered controversial, to the point that Google style guide advises against their use without any& exception.

In this article, I take the side of advocating for a correct use of C++ exceptions.

But first thing first, I want to dismiss right away an argument against the usage of C++ exceptions I hear too often, and that isn’t helpful to the conversation.

C++ exceptions are easily misused.

Yes, they are. As every single powerful feature offered by every single programming language written top of Assembly, it’s easy to misuse C++ exceptions. This is why software engineers have the responsibility to acquire the necessary expertise, and master the most fitting tool to solve the problem at hand.

Quality craft doesn’t have any use for tool blaming.

Why I like exceptions

There are three main reasons why I like using C++ exceptions as error handling tools. Not in any particular order, they are:

  • By relieving the main logic from the need of considering each possible error on the spot, they increase overall readability and performance.
  • By decoupling the error handing code, they increase cohesion, separation of concerns and flexibility of the user code.
  • By providing a standardised way to express error conditions, they reduce the amount of context specific knowledge required understand the code.

Exceptions have also pitfalls that need to be addressed (even if they are correctly used):

  • Once exceptions are introduced, it’s not easy, and at times it’s not possible, to know which operations may result in an early exit of the code flow.
  • Similarly, it can be hard to impossible to determine which exceptions may be thrown by a certain code, and so, which ones should be handled.
  • Uncaught exceptions are hidden asserts. They are meant to be dealt with, but they actually terminate the host program.

However, I don’t see necessarily these pitfalls as drawbacks. They are potential problems that need to be addressed, but addressing them correctly implies writing more robust, readable and flexible code.

Exceptions make code faster

When an error condition is detected, throwing exceptions is many orders of magnitude slower than returning an error code. According with simple measurements, they can be over 100 times slower. However, that cost is paid only when actually throwing an exception, which is not meant to happen very often. In exchange for that, the cost paid in the non-exceptional case is zero, while the cost of returning and checking a status code is always paid.

Consider the following example:

void change(std::string& someParam) {
  if(someParam.empty()) {
    throw std::runtime_error("The parameter should not be empty");
  }
  if(!checkValid(someParam)) {
    throw std::runtime_error("The parameter is not valid");
  }
  //... Do something useful with someParam...
}

std::string changeAndMerge(std::string value1, std::string value2, std::string value3) {
  try{
    change(value1);
    change(value2);
    change(value3);
    return value1 + value2 + value3;
  }
  catch(const std::runtime_error& error) {
    std::cout << "Huston, we have a problem: " << error.what() << '\n';
    return "";
  }
}

On line 13 onwards, the calls to change() are flowing seamlessly. Passing that code to an assembler, it’s easy to see that there is no conditional jump, and the catch{} clause is entered directly by setting the instruction pointer to that address only when an exception is actually thrown.

Conversely see the same program dealing with an error code:

enum class ErrorCode{
  OK,
  Empty,
  Invalid,
  // ... more codes
};

ErrorCode change(std::string& someParam) {
  if(someParam.empty()) {
    return ErrorCode::Empty;
  }
  if(!checkValid(someParam)) {
    return ErrorCode::Invalid;
  }
  //... Do something useful with someParam...
  return ErrorCode::OK;
}

std::string changeAndMerge(std::string value1, std::string value2, std::string value3) {
  auto result = change(value1);
  if(result != ErrorCode::OK) {
    std::cout << "Huston, we have a problem: " << result << '\n';
    return "";
  }

  result = change(value2);
  if(result != ErrorCode::OK) {
    std::cout << "Huston, we have a problem: " << result << '\n';
    return "";
  }

  result = change(value3);
  if(result != ErrorCode::OK) {
    std::cout << "Huston, we have a problem: " << result << '\n';
    return "";
  }

  return value1 + value2 + value3;
}

Now each time we call change() we need to return something that wasn’t necessary before, and then check for it not to be an error. This is always required, no matter how sporadic the problem found in change() may be  for example notice that in the case above, if we provide pre-validated inputs, the exception may never be thrown during the whole lifetime of the program.

In modern CPUs the cost of branching is much higher than the time spent in processing the actual CMP/JMP or equivalent opcodes. Branching prevents micro-code optimisations and predictive execution strategies, which are what make modern CPUs so much faster than older ones (for example see this video)

So, by renouncing to exceptions, you are also renouncing to the optimisation opportunities offered by large part of your code base being branchless.

Readability

The two previous snippets also show how the code using exceptions is objectively more readable than the one using error codes. While the specific example may be refactored to make it a little bit nicer, and avoid the repetitions, in practice it’s usually necessary to have custom error management code snippets right at the spot where every single error is detected.

According with the principle of minimal incompleteness, having to open a context in which the normal flow of the current processing is suspended in order to deal with the error, and then having to resume the previous logic flow after the error is dealt with (e.g. on line 21) creates areas of higher incompleteness in the code, which makes it quantitatively less readable.

Flexibility

By separating the concerns of program flow from error management, exceptions increase the cohesion of the user code. In the first example, lines 13 to 16 are completely dedicated to program flow, while all the error management is delegated to the catch{} clause; conversely, in simple_error_code.cpp the body of checkAndMerge needs to deal with error management throughout its body.

Any change to either the structure of the error (i.e. a change in the name of the error code symbols) or to its representation (i.e. how it’s reported) will require more extensive changes in simple_error_code.cpp, which means that its code is less flexible by definition.

This is even more evident when a function traversed by the error doesn’t have the context to know how to manage it. In the next example, changeAndMerge isn’t anymore able to decide what to do when an error is encountered; a function at a higher level decides to store the error status into a transiting object, which is the final recipient of the operations changeAndMerge wants to perform.

void change(std::string& someParam) {
  if(someParam.empty()) {
    throw std::runtime_error("The parameter should not be empty");
  }
  if(!checkValid(someParam)) {
    throw std::runtime_error("The parameter is not valid");
  }
  //... Do something useful with someParam...
}

std::string changeAndMerge(std::string value1, std::string value2, std::string value3) {
    change(value1);
    change(value2);
    change(value3);
    return value1 + value2 + value3;
}

void updateContext(SomeClass& someContext) {
  try {
    someContext.setResult(
      changeAndMerge(someContext.name(), someContext.middle(), someContext.surname()));
  }
  catch(const std::runtime_error& error) {
    someContext.setError(error.what());
  }
}

As you can see, changeAndMerge (on line 11) is completely agnostic about both what happen in change() and what will be done to manage potential errors. All this knowledge is stored in updateContext(), which knows both what to do with a correct result (on line 20) and manage any error that might be generated in the process (on line 24).

Any change in how error is generated in change(), or in how it’s managed in updateContext() will have minimal impact on the code, which makes it maximally flexible.

By contrast, consider this code snippet where changeAndMerge() needs to handle any error change() may produce, and updateContext() only knows errors that could be generated by changeAndMerge().

enum class ErrorCode{
  OK,
  Empty,
  Invalid,
  // ... more codes
};

ErrorCode change(std::string& someParam) {
  if(someParam.empty()) {
    return ErrorCode::Empty;
  }
  if(!checkValid(someParam)) {
    return ErrorCode::Invalid;
  }
  //... Do something useful with someParam...
  return ErrorCode::OK;
}

enum class StatusCode {
  OK,
  CouldntDoIt,
  // ... more codes
};

StatusCode changeAndMerge(std::string& result,
                     std::string value1, std::string value2, std::string value3) {
  if(change(value1) != ErrorCode::OK) {
    // Shall we log/save/propagate the actual ErrorCode?
    return StatusCode::CouldntDoIt;
  }
  if(change(value2) != ErrorCode::OK) {
    return StatusCode::CouldntDoIt;
  }
  if(change(value3) != ErrorCode::OK) {
    return StatusCode::CouldntDoIt;
  }
  result = value1 + value2 + value3;
  return StatusCode::OK;
}

void updateContext(SomeClass& someContext) {
  std::string result;
  StatusCode status = changeAndMerge(result,
      someContext.name(), someContext.middle(), someContext.surname()));
  if(status == StatusCode::OK) {
    someContext.setResult(result);
  }
  else {
    someContext.setError(translateStatusCode(status));
  }
}

Here error management is completely intermixed with the normal flow. Any change to changeAndMerge() requires to know both how updateContext() wants to handle its return code, and how change() will communicate errors. Other than being maximally incomplete, this also reduces the flexibility of the code: since the concern of handling errors and that of actually performing normal operations are not separated, any change to the overall logic of the error management requires to redesign most of it.

What’s worse, even changes to the normal flow of operations are now made more burdensome. What if we introduce another call that may fail after line 36? Should we add a different return code for changeAndMerge()? Would updateContext() be required to perform additional operations after line 49?

The fact that the three functions are now so tightly coupled not in their functionality, but in how they exchange information about error statuses, bleeds interdependency into every aspect of their code, making extending, optimising, and refactoring much more complex.

Cohesion in exception handling

Exceptions are designed to gather all the information relevant to address and/or report an error condition at the spot where it’s detected.

  • The detection code has all the necessary context to store up any information required.
  • The management code has all the context required to address the exceptional condition.
  • The two areas of the code communicate through a standardised interface, the exception object itself, which breaks any coupling between the two.

By contrast, returning an error code requires the caller to leverage its own knowledge of the context in which the error was encountered; this means that the error must be dealt with immediately at the caller’s spot. This becomes a problem when the caller doesn’t have the context to actually address the error. At that point, the caller has to communicate back an extended context to the higher level program logic; in the end, all the logic chain from the topmost level down to where the error is detected requires some knowledge of the surrounding context, in both upwards and downards directions, in order to deal effectively with relevant error conditions.

A way to address this situation without the exception throwing mechanism is that of returning a result object, which encapsulates either the desired output or an error object; it becomes the protocol through which the detection code and the handling code can communicate.

However, this model exacerbates some of the drawbacks of error management through returned error codes. For example, see the following minimal implementation of a result based error management.

template<class R, class E>
class Result {
public:
  // Omitting forward typedefs for simplicity.

  Result{R&& result, E&& error}: result_{std::move(result)}, error_{std::move(error)}
  {}
  Result{R&& result}: result_{std::move(result)}, error_{std::nullopt}
  {}
  Result{E&& error}: result_{std::nullopt}, error_{std::move(error)}
  {}

  operator bool() const {
    return ! error.has_value();
  }

  R& operator*() {
    if(result_.has_value()) {
      return result_.value();
    }
    assert(false);
    // Add logging, alerts, or just let std::bad_access be thrown by std::optional.
  }

  // Omitting other cast operators for simplicity.

  const E& getError() const {
    if(error_.has_value()) {
      return *error_;
    }
    assert(false);
  }
private:
  std::optional<R> result_;
  std::optional<E> error_;
};

enum class ErrorCode{
  OK,
  Empty,
  Invalid,
  // ... more codes
};

Result<std::string, ErrorCode> change(const std::string& someParam) {
  if(someParam.empty()) {
    return {ErrorCode::Empty};
  }
  if(!checkValid(someParam)) {
    return {ErrorCode::Invalid};
  }
  std::string changed = someParam;
  //... Do something useful with changed...
  return {changed};
}

enum class StatusCode {
  OK,
  CouldntDoIt,
  // ... more codes
};

Result<std::string, StatusCode> changeAndMerge(
                     std::string value1, std::string value2, std::string value3) {
  auto result1 = change(value1);
  if(!result1) {
    // Shall we log/save/propagate the actual ErrorCode?
    return {StatusCode::CouldntDoIt};
  }
  auto result2 = change(value2);
  if(!result2) {
    return {StatusCode::CouldntDoIt};
  }
  auto result3 = change(value3);
  if(!result3) {
    return {StatusCode::CouldntDoIt};
  }
  return{*value1 + *value2 + *value3};
}

void updateContext(SomeClass& someContext) {
  auto result = changeAndMerge(
      someContext.name(), someContext.middle(), someContext.surname()));
  if(result) {
    someContext.setResult(*result);
  }
  else {
    someContext.setError(translateStatusCode(result.getError()));
  }
}

While this solution allows for adding any relevant information to the error object (here I am still using a numeric code for simplicity), and that might be decoupled from the context where the error was detected, we have actually made our problems worse:

  • Returning a non-error result in the ordinary path is more expensive than before.
  • Checking the returned value for errors has become more expensive too, with even more branching involved.
  • Using the returned non-error object will require at least an additional branch and memory dereference, which may cause additional cache misses, further compromising the possibility for runtime optimisations.
  • When using a non-trivial object to store the error context, the advantage of returning the Result structure with respect to using the throw statement becomes negligible.

But the worst problem I see with this solution is that it can’t be used to break the couplings between caller and callee with regards to error management. Unless we can guarantee that a single error type is used throughout the program (i.e. completely removing the template type in Result declaration), each non-trivial function will need to re-define an error type it wants to return to its caller, and the caller will have to understand it and possibly re-encapsulate it in its own error condition.

In short, even this solution, that at first glance seems to gain back some flexibility, ends up making the program more coupled, and so, more rigid.

Avoiding pitfalls

C++ exceptions are a powerful tool for handling unexpected conditions in programs, but as all the powerful tools, their usage require particular care.

Use exceptions only& as an exception

This is the most important rule concerning C++ exceptions: throwing them should be rare. Ideally, during an ordinary run, a well-formed program with sanitised inputs and a stable running environment should never have the need for exceptions to be actually thrown.

The rule of thumb is that C++ exceptions should be used only when less than one over a thousands operations are expected to fail, or when the program flow is slow by design (i.e. it’s reacting to a UI input from a human user).

More formally, C++ exceptions are to be used only to deal with unexpected conditions, where the definition of “unexpected” must be clearly established at the higher program logic level.

For example, picture a file or network address scanner, which will detect existing targets by identifying a successful operation after many attempts that are expected to fail. In that case, the failures are not errors, but expected results meant to be handled at the lowest logical level possible, right where the I/O operation is performed.

Conversely, imagine the case of a program trying to open a file or a network address given as a command line option or in configuration. Then, we do expect to complete the operation successfully, and failure to do so requires the highest program logic to be put in charge of how to handle the problem.

In the first case, C++ exceptions would only get in the way; in the second, they provide a clean and powerful method to put the main logic back at the helm when the program is facing an extreme situation.

This is actually the rationale behind the standard I/O library having optional exceptions. The user can decide wether failures on a certain stream should return an error code or raise an exception; this is because the fact of the error on a stream being an unexpected condition depends only on what the user code expects, and is not known in advance by the library writer.

Always expect the unexpected

Once exceptions are introduced in a code base, the user code must always be prepared to either catch them or be interrupted at any time.

This means that such programs must ensure that invariants are always respected and all entity states are always consistent, or in case they need to temporarily break invariants and put entities in undetermined states, they need to take measures to prevent being interrupted in the process.

While this is a pitfall when using exceptions naively, I don’t see it as a drawback. Writing programs that respect this contract is consistently superior to not doing so. It’s a design effort that leads to more robust, flexible and readable results. Once mastered, it’s a no-tradeoff design where superior quality leads to higher performance and lower overall development time.

Consider the following example:

struct SomeData {};

SomeData* makeFirst() { doSomeOps(); // might throw return new SomeData; }

SomeData* makeSecond() { doSomeOps(); // might throw return new SomeData; }

std::string mergeData(SomeData* first, SomeData* second) { // Analyse first and second; auto result = doSomethingWith(first, second); delete first; delete second; return result; }

std::string doThings() { return mergeData(makeFirst(), makeSecond()); }


The function `doThings()` is not exception-safe; if the operation on line 9 fails, the result of `makeFirst()` will be leaked.

The correct approach in modern C++ is that of using types that manage their scope automatically; this is called _resource allocation is initializtion (RAII)_ idiom. For example:

```C++
struct SomeData {};

auto makeFirst() {
  doSomeOps(); // might throw
  return std::make_unique<SomeData>();
}

auto makeSecond() {
  doSomeOps(); // might throw
  return std::make_unique<SomeData>();
}

std::string mergeData(SomeData& first, SomeData& second)
{
  // Do something meaningful with first and second.
  return "Merged data";
}

std::string doThings() {
  auto firstData = makeFirst();
  auto secondData = makeSecond();
  return mergeData(*firstData, *secondData);
}

Now doThings() is not just exception-safe; if it ever grows to be a much larger function, it will be safe to return from it at any point, without having to track what’s the current status of firstData and secondData. For the principle of minimal incompleteness, this is a more readable solution, and it happens also to be more flexible and less error prone than any code that depends on not being interrupted at any point.

Know your exceptions

One criticism to the C++ exception system is that it becomes impossible to know which exception can be thrown by any function very fast, no matter how carefully possible exceptions are documented. As such, it’s impossible to know what to catch and where, in order to make a piece of code exception-free.

This criticism misses completely the point of error management through the C++ exception system.

The error handling code shouldn’t handle any exception that the managed code may raise, but only and all the exceptions that it knows how to handle, regardless if the managed code can actually throw those exceptions or not.

This is because:

  1. The managed code may not throw a certain exception now, but it might do it in future. It’s important that, if the exception has a meaning in the manager context, what to do with it is already determined at an early stage of development, before the user code has even a chance to actually encounter that condition.
  2. Modern C++ is a much more dynamic language than what it was in the past. With easy-to-use functional extensions as std::function and lambdas, it becomes increasingly possible to run arbitrary code at any level of the logic. As such, C++ programs must be designed to be resilient against unexpected behaviour, and not rely on deeper level code to have particular constraints unless explicit actions are taken in that direction.
  3. By design, the C++ exception system is meant to encapsulate all the context needed to know what went wrong at an arbitrary point of the execution and deliver this information to any actor interested in dealing with the problem, or terminate the program if none can. Exceptions handlers should be written according with this design, and not forcing other models into it.

Uncaught exceptions are hidden assertions

A slightly more sophisticated criticism of the C++ exception system extends the previous point: as it’s not possible to determine which excecptions may be thrown by an arbitrary code section in the program, it’s likely that an uncaught exception will terminate the program unexpectedly.

Again, this criticism is only valid against a very naive usage of the system; the language and the standard library provide several tools that prevent this from ever becoming a problem.

The first rule of thumb is that all user-defined exceptions should be derived from std::exception, or std::runtime_error when appropriate.

The class std::exception provides a what() virtual method that gives any user-defined error system the ability to render a human readable description. In this way, any code in need to handle any possible error can fall back to catch std::exception, if everything else fails, and produce a sensible warning for a human user to read, so that the program can be fixed in order to deal with the error properly.

If the code being written is not part of a library, but it’s meant to be used only in a specific application, it’s also possible to create a completely separate exception class hyerarchy; catching std::exception is granted to catch any standard specific exception, while catching an application-specific base error class is granted to engage an application-specific error management.

As a last line of defence against undisciplined third party code, which may throw exceptions not respecting any design requirement, the standard library provdes std::current_exception to get some information on any object caught by the last ditch defence, thecatch(&) clause.

Not that you really should always catch any possible exception. Crashing a program on uncaught exception is not a bug; it’s a feature. You have the option to catch anything at the highest application logic level (i.e. in main() or in the first place where you can report a fatal error through an established logging or alert system), but wether that option is good for you or not depends on the context.

In some cases, it might be a better design option to let the exception uncaught and have the standard C++ library printing a meaningful termination message, or see the exception unfold through a post-mortem debug.

Is the reliability of the program so crucial that it should never go down, no matter how terrible the unexpected situation is? Or is the precision of the result so crucial that it’s better to stop a program encountering an unexpected condition, and having a human supervisor resolving the situation?

The C++ exception system gives the developers the tools to decide which solution is better for them.

Are exceptions good for you?

C++ exceptions are an excellent device to handle exceptional conditions encountered during program execution.

They are also part of the language specification and the standard library: they are designed as the only proper way to handle failures during object creation, and to report other critical problems met by standard operators (as new) and library functions.

As such, you should already be mindful of exceptions, and take all necessary steps to write exception-safe code.

For this reason, adding domain-specific exceptions is not just natural, but the preferred way to handle unexpected conditions in C++.

However, it’s important to correctly define what is “exceptional” enough for it to be handled through this powerful device.

Not all errors are exceptions. In some cases, we do expect errors to happen; that’s where C++ exceptions should not be used, and a different, more localised and direct error handling is in order.

Performance considerations also play a role: whether we expect a certain error to happen or not, if it appears to happen more than 0.1% of the times, the machinery needed for handling C++ exceptions may be too expensive.

Since it’s not always possible to know if a certain error condition is considered exceptional or not, it may also be necessary to give the user code the choice of throwing exception or not, as in the case of the standard I/O library.

But whatever the choice ends to be, C++ exceptions are a powerful, integral part of the language, and solve a class of difficult engineering problems effectively and elegantly. They should not be excluded a-priori from the toolset that software engineers are allowed to use, no matter how “easily misused” they are.