[ Home | Resume | Programming | Engineering Philosophy | Family ]

Guidelines For Error Handling In C++

Don't Rule Out Exceptions

If you preclude exceptions, then proper error handling becomes prohibitive in many cases. Furthermore, you prevent yourself from integrating any code that is capable of throwing any exceptions.

Not ruling out exceptions means writing exception-safe code, which requires some additional care, but is not difficult once you get the hang of it. It also means that you can't rely on taking advantage of compiler options that might generate code that is slightly more efficient than exception-capable code. These costs are almost always worth the benefits in the long run.

Detected Program Errors Result in abort()

If a program detects an error in its own code, then it should call abort() such that the error can be debugged. The bug detection code should be disabled when NDEBUG is defined. In particular, a module (e.g. a class) should detect and report failures of its invariants this way.

You need to be careful about what you call a bug in the client code. Defining every case that you don't want to deal with as a "caller error" leads to poor runtime efficiency and poor code economy, because it burdens all of a module's clients. For example, a queue class should either allow pop() when it is empty, provide an empty detection method, or both.

Error Codes vs. Exceptions

Error conditions that are not necessarily program errors should instead be indicated to the caller, who can then decide what to do about it. There are two ways to indicate an error condition to the caller: error codes and exceptions.

Error codes have the advantage of being efficiently propagated. However, they also have the disadvantage of requiring the immediate caller to handle them, which makes the code more difficult to maintain, especially when the callee then needs to return a struct because it already had a return value. Furthermore, if the caller doesn't handle the error itself (possibly by converting it to an exception), then the caller's caller also needs to deal with an error code, and so on.

Exceptions have the advantage of being automatically propagated to the nearest outer scope that is prepared to deal with them, without requiring any additional program code to direct this propagation. However, the "stack unwinding" code that is executed when an exception actually occurs takes a long time to execute (typically much longer than the overhead of an ordinary function call).

As a rule, the stack unwinding overhead should not exceed 10% of the available execution time under typical conditions, and should not exceed 50% of the available execution time under any conceivable conditions. If that isn't the case, then you should be using error codes rather than exceptions in the areas that account for the greatest portion of the overhead. (We generally assume that efficiency is an important concern in C++ programming, because otherwise you could save yourself a lot of trouble by using Java or Perl instead.)

It is often the case that a method cannot tell whether error codes or exceptions are more appropriate for each of its clients. In those cases, it is recommended that two methods be provided: one that returns an error code, and a second one that wraps the first, converting any errors into exceptions.

Prefer the Strong Guarantee

Because we don't want to rule out exceptions, it is assumed that all code is required to be exception safe, meaning that it satisfies the so-called Basic guarantee (no resources are leaked in light of exceptions). In order to satisfy the Basic guarantee, it is generally required that all destructors satisfy the NoThrow guarantee (no exceptions can escape the call). See GOTW #47 .

If an error condition is propagated to the caller, then regardless of whether it is propagated via error codes or exceptions, in most cases you'll want to satisfy the Strong guarantee. The Strong guarantee dictates that if an error or exception escapes the call, then the call has no visible side effects (other than the possible logging of events, provided that the fact that such events are reverted due to an exception is also logged). More precisely, if an error or exception escapes a function satisfying the Strong guarantee, then any subsequent behavior of the program (disregarding performance, logging, execution statistics, memory fragmentation, etc.) satisfies all guarantees that would have held had the function call been replaced by a corresponding error code value or throw statement.

Avoid non-const member functions that return an object by value, because they make it impossible for the caller to satisfy the Strong guarantee. See GOTW #8 . Instead split it into two different methods, or assign the result through a reference parameter. (Presumably, if return by reference were suitable, then you'd already be doing that.)

There are cases in which the Strong guarantee is impossible, or its runtime and memory costs are impractical. In particular, general purpose code (such as container classes) that might be used in such a case must violate the Strong guarantee, in which case such code may be wrapped by code that causes it to be satisfied.

In any case, it should always be clear which guarantee holds for a given call. A reasonable convention is to assume the Strong guarantee unless otherwise noted.

Error Handling is part of Up-front Design

Although efficiency is an important consideration in error handling design, it's not a good idea to wait until you profile the code to consider the trade-offs. That's because many of the changes that you might like to make for the sake of efficiency will violate existing properties of the module that clients have come to rely upon.

For example, a method that aborts on a particular error might be expected to satisfy the NoThrow guarantee. If you renege on that by throwing an exception on the error, then you jeopardize all of your module's clients, which is generally unacceptable. Instead, you can provide a new method that converts the error to an exception, but you'll still need to change, possily at some considerable expense, all of the client code that wants to take advantage of this new method.

As with all other aspects of program design, you are well advised to spend a lot of time up front to consider all the implications of the design, including gross assessments of efficiency, before detailed coding. This will minimize the need for expensive late redesign.

Anders Johnson, last modified $Date: 2003/01/08 $

[ Home | Resume | Programming | Engineering Philosophy | Family ]