E: Error handling

Error handling involves:

  • Detecting an error
  • Transmitting information about an error to some handler code
  • Preserve the state of a program in a valid state
  • Avoid resource leaks

It is not possible to recover from all errors. If recovery from an error is not possible, it is important to quickly "get out" in a well-defined way. A strategy for error handling must be simple, or it becomes a source of even worse errors. Untested and rarely executed error-handling code is itself the source of many bugs.

The rules are designed to help avoid several kinds of errors:

  • Type violations (e.g., misuse of unions and casts)
  • Resource leaks (including memory leaks)
  • Bounds errors
  • Lifetime errors (e.g., accessing an object after is has been deleted)
  • Complexity errors (logical errors make likely by overly complex expression of ideas)
  • Interface errors (e.g., an unexpected value is passed through an interface)

Error-handling rule summary:

E.1: Develop an error-handling strategy early in a design

Reason

A consistent and complete strategy for handling errors and resource leaks is hard to retrofit into a system.

E.2: Throw an exception to signal that a function can't perform its assigned task

Reason

To make error handling systematic, robust, and non-repetitive.

Example
struct Foo {
    vector<Thing> v;
    File_handle f;
    string s;
};

void use()
{
    Foo bar {{Thing{1}, Thing{2}, Thing{monkey}}, {"my_file", "r"}, "Here we go!"};
    // ...
}

Here, vector and strings constructors may not be able to allocate sufficient memory for their elements, vectors constructor may not be able copy the Things in its initializer list, and File_handle may not be able to open the required file. In each case, they throw an exception for use()'s caller to handle. If use() could handle the failure to construct bar it can take control using try/catch. In either case, Foo's constructor correctly destroys constructed members before passing control to whatever tried to create a Foo. Note that there is no return value that could contain an error code.

The File_handle constructor might defined like this:

File_handle::File_handle(const string& name, const string& mode)
    :f{fopen(name.c_str(), mode.c_str())}
{
    if (!f)
        throw runtime_error{"File_handle: could not open "S-+ name + " as " + mode"}
}
Note

It is often said that exceptions are meant to signal exceptional events and failures. However, that's a bit circular because "what is exceptional?" Examples:

  • A precondition that cannot be met
  • A constructor that cannot construct an object (failure to establish its class's invariant)
  • An out-of-range error (e.g., v[v.size()] =7)
  • Inability to acquire a resource (e.g., the network is down)

In contrast, termination of an ordinary loop is not exceptional. Unless the loop was meant to be infinite, termination is normal and expected.

Note

Don't use a throw as simply an alternative way of returning a value from a function.

Exception: Some systems, such as hard-real time systems require a guarantee that an action is taken in a (typically short) constant maximum time known before execution starts. Such systems can use exceptions only if there is tool support for accurately predicting the maximum time to recover from a throw.

See also: RAII

See also: discussion

Note

Before deciding that you cannot afford or don't like exception-based error handling, have a look at the alternatives.

E.3: Use exceptions for error handling only

Reason

To keep error handling separated from "ordinary code." C++ implementations tend to be optimized based on the assumption that exceptions are rare.

Example, don't
int find_index(vector<string>& vec, const string& x)   // don't: exception not used for error handling
{
    try {
        for (int i =0; i < vec.size(); ++i)
            if (vec[i] == x) throw i;  // found x
    } catch (int i) {
        return i;
    }
    return -1;   // not found
}

This is more complicated and most likely runs much slower than the obvious alternative. There is nothing exceptional about finding a value in a vector.

E.4: Design your error-handling strategy around invariants

Reason

To use an object it must be in a valid state (defined formally or informally by an invariant) and to recover from an error every object not destroyed must be in a valid state.

Note

An invariant is logical condition for the members of an object that a constructor must establish for the public member functions to assume.

E.5: Let a constructor establish an invariant, and throw if it cannot

Reason

Leaving an object without its invariant established is asking for trouble. Not all member functions can be called.

Example
???

See also: If a constructor cannot construct a valid object, throw an exception

Enforcement

???

E.6: Use RAII to prevent leaks

Reason

Leaks are typically unacceptable. RAII ("Resource Acquisition Is Initialization") is the simplest, most systematic way of preventing leaks.

Example
void f1(int i)   // Bad: possibly leak
{
    int* p = new int[12];
    // ...
    if (i < 17) throw Bad {"in f()", i};
    // ...
}

We could carefully release the resource before the throw:

void f2(int i)   // Clumsy: explicit release
{
    int* p = new int[12];
    // ...
    if (i < 17) {
        delete[] p;
        throw Bad {"in f()", i};
    }
    // ...
}

This is verbose. In larger code with multiple possible throws explicit releases become repetitive and error-prone.

void f3(int i)   // OK: resource management done by a handle
{
    auto p = make_unique<int[]>(12);
    // ...
    if (i < 17) throw Bad {"in f()", i};
    // ...
}

Note that this works even when the throw is implicit because it happened in a called function:

void f4(int i)   // OK: resource management done by a handle
{
    auto p = make_unique<int[]>(12);
    // ...
    helper(i);   // may throw
    // ...
}

Unless you really need pointer semantics, use a local resource object:

void f5(int i)   // OK: resource management done by local object
{
    vector<int> v(12);
    // ...
    helper(i);   // may throw
    // ...
}
Note

If there is no obvious resource handle, cleanup actions can be represented by a final_action object

Note

But what do we do if we are writing a program where exceptions cannot be used? First challenge that assumption; there are many anti-exceptions myths around. We know of only a few good reasons:

  • We are on a system so small that the exception support would eat up most of our 2K or memory.
  • We are in a hard-real-time system and we don't have tools that guarantee us that an exception is handled within the required time.
  • We are in a system with tons of legacy code using lots of pointers in difficult-to-understand ways (in particular without a recognizable ownership strategy) so that exceptions could cause leaks.
  • We get fired if we challenge our manager's ancient wisdom.

Only the first of these reasons is fundamental, so whenever possible, use exceptions to implement RAII, or design your RAII objects to never fail. When exceptions cannot be used, simulate RAII. That is, systematically check that objects are valid after construction and still release all resources in the destructor. One strategy is to add a valid() operation to every resource handle:

void f()
{
    vector<string> vs(100);   // not std::vector: valid() added
    if (!vs.valid()) {
        // handle error or exit
    }

    Ifstream fs("foo");   // not std::ifstream: valid() added
    if (!fs.valid()) {
        // handle error or exit
    }

    // ...
} // destructors clean up as usual

Obviously, this increases the size of the code, doesn't allow for implicit propagation of "exceptions" (valid() checks), and valid() checks can be forgotten. Prefer to use exceptions.

See also: discussion.

Enforcement

???

E.7: State your preconditions

Reason

To avoid interface errors.

See also: precondition rule.

E.8: State your postconditions

Reason

To avoid interface errors.

See also: postcondition rule.

E.12: Use noexcept when exiting a function because of a throw is impossible or unacceptable

Reason

To make error handling systematic, robust, and efficient.

Example
double compute(double d) noexcept
{
    return log(sqrt(d <= 0 ? 1 : d));
}

Here, I know that compute will not throw because it is composed out of operations that don't throw. By declaring compute to be noexcept I give the compiler and human readers information that can make it easier for them to understand and manipulate compute.

Note

Many standard library functions are noexcept including all the standard library functions "inherited" from the C standard library.

Example
vector<double> munge(const vector<double>& v) noexcept
{
    vector<double> v2(v.size());
    // ... do something ...
}

The noexcept here states that I am not willing or able to handle the situation where I cannot construct the local vector. That is, I consider memory exhaustion a serious design error (on par with hardware failures) so that I'm willing to crash the program if it happens.

See also: discussion.

E.13: Never throw while being the direct owner of an object

Reason

That would be a leak.

Example
void leak(int x)   // don't: may leak
{
    auto p = new int{7};
    if (x < 0) throw Get_me_out_of_here{}  // may leak *p
    // ...
    delete p;   // we may never get here
}

One way of avoiding such problems is to use resource handles consistently:

void no_leak(int x)
{
    auto p = make_unique<int>(7);
    if (x < 0) throw Get_me_out_of_here{};  // will delete *p if necessary
    // ...
    // no need for delete p
}

See also: ???resource rule ???

E.14: Use purpose-designed user-defined types as exceptions (not built-in types)

Reason

A user-defined type is unlikely to clash with other people's exceptions.

Example
void my_code()
{
    // ...
    throw Moonphase_error{};
    // ...
}

void your_code()
{
    try {
        // ...
        my_code();
        // ...
    }
    catch(Bufferpool_exhausted) {
        // ...
    }
}
Example, don't
void my_code()     // Don't
{
    // ...
    throw 7;       // 7 means "moon in the 4th quarter"
    // ...
}

void your_code()   // Don't
{
    try {
        // ...
        my_code();
        // ...
    }
    catch(int i) {  // i == 7 means "input buffer too small"
        // ...
    }
}
Note

The standard-library classes derived from exception should be used only as base classes or for exceptions that require only "generic" handling. Like built-in types, their use could clash with other people's use of them.

Example, don't
void my_code()   // Don't
{
    // ...
    throw runtime_error{"moon in the 4th quarter"};
    // ...
}

void your_code()   // Don't
{
    try {
        // ...
        my_code();
        // ...
    }
    catch(runtime_error) {   // runtime_error means "input buffer too small"
        // ...
    }
}

See also: Discussion

Enforcement

Catch throw and catch of a built-in type. Maybe warn about throw and catch using an standard-library exception type. Obviously, exceptions derived from the std::exception hierarchy is fine.

E.15: Catch exceptions from a hierarchy by reference

Reason

To prevent slicing.

Example
void f()
try {
    // ...
}
catch (exception e) {   // don't: may slice
    // ...
}

Instead, use:

catch (exception& e) { /* ... */ }
Enforcement

Flag by-value exceptions if their types are part of a hierarchy (could require whole-program analysis to be perfect).

E.16: Destructors, deallocation, and swap must never fail

Reason

We don't know how to write reliable programs if a destructor, a swap, or a memory deallocation fails; that is, if it exits by an exception or simply doesn't perform its required action.

Example, don't
class Connection {
    // ...
public:
    ~Connection()   // Don't: very bad destructor
    {
        if (cannot_disconnect()) throw I_give_up{information};
        // ...
    }
};
Note

Many have tried to write reliable code violating this rule for examples such as a network connection that "refuses to close". To the best of our knowledge nobody has found a general way of doing this though occasionally, for very specific examples, you can get away with setting some state for future cleanup. Every example we have seen of this is error-prone, specialized, and usually buggy.

Note

The standard library assumes that destructors, deallocation functions (e.g., operator delete), and swap do not throw. If they do, basic standard library invariants are broken.

Note

Deallocation functions, including operator delete, must be noexcept. swap functions must be noexcept. Most destructors are implicitly noexcept by default.

Enforcement

Catch destructors, deallocation operations, and swaps that throw. Catch such operations that are not noexcept.

See also: discussion

E.17: Don't try to catch every exception in every function

Reason

Catching an exception in a function that cannot take a meaningful recovery action leads to complexity and waste. Let an exception propagate until it reaches a function that can handle it. Let cleanup actions on the unwinding path be handled by RAII.

Example, don't
void f()   // bad
{
    try {
        // ...
    }
    catch (...) {
        throw;   // propagate exception
    }
}
Enforcement
  • Flag nested try-blocks.
  • Flag source code files with a too high ratio of try-blocks to functions. (??? Problem: define "too high")

E.18: Minimize the use of explicit try/catch

Reason

try/catch is verbose and non-trivial uses error-prone. try/catch can be a sign of unsystematic and/or low-level resource management or error handling.

Example, Bad
void f(zstring s)
{
    Gadget* p;
    try {
        p = new Gadget(s);
        // ...
    }
    catch (Gadget_construction_failure) {
        delete p;
        throw;
    }
}

This code is messy. There could be a leak from the naked pointer in the try block. Not all exceptiones are handled. deleting an object that failed to construct is almost certainly a mistake. Better:

void f2(zstring s)
{
    Gadget g {s};
}
Alternatives
Enforcement

??? hard, needs a heuristic

E.19: Use a final_action object to express cleanup if no suitable resource handle is available

Reason

finally is less verbose and harder to get wrong than try/catch.

Example
void f(int n)
{
    void* p = malloc(1, n);
    auto _ = finally([p] { free(p); });
    // ...
}
Note

finally is not as messy as try/catch, but it is still ad-hoc. Prefer proper resource management objects.

E.25: If you can't throw exceptions, simulate RAII for resource management

Reason

Even without exceptions, RAII is usually the best and most systematic way of dealing with resources.

Note

Error handling using exceptions is the only complete and systematic way of handling non-local errors in C++. In particular, non-intrusively signalling failure to construct an object requires an exception. Signalling errors in a way that cannot be ignored requires exceptions. If you can't use exceptions, simulate their use as best you can.

A lot of fear of exceptions is misguided. When used for exceptional circumstances in code that is not littered with pointers and complicated control structures, exception handling is almost always affordable (in time and space) and almost always leads to better code. This, of course, assumes a good implementation of the exception handling mechanisms, which is not available on all systems. There are also cases where the problems above do not apply, but exceptions cannot be used for other reasons. Some hard real-time systems are an example: An operation has to be completed within a fixed time with an error or a correct answer. In the absence of appropriate time estimation tools, this is hard to guarantee for exceptions. Such systems (e.g. flight control software) typically also ban the use of dynamic (heap) memory.

So, the primary guideline for error handling is "use exceptions and RAII." This section deals with the cases where you either do not have an efficient implementation or exceptions or have such a rat's nest of old-style code (e.g., lots of pointers, ill-defined ownership, and lots of unsystematic error handling based on tests of errors codes) that it is infeasible to introduce simple and systematic exception handling.

Before condemning exceptions or complaining too much about their cost, consider examples of the use of error codes.

Example

Assume you wanted to write

void func(int n)
{
    Gadget g(n);
    // ...
}

If the gadget isn't correctly constructed, func exits with an exception. If we cannot throw an exception, we can simulate this RAII style of resource handling by adding a valid() member function to Gadget:

error_indicator func(int n)
{
    Gadget g(n);
    if (!g.valid()) return gadget_construction_error;
    // ...
    return 0;   // zero indicates "good"
}

The problem is of course that the caller now have to remember to test the return value.

See also: Discussion.

Enforcement

Possible (only) for specific versions of this idea: e.g., test for systematic test of valid() after resource handle construction

E.26: If you can't throw exceptions, consider failing fast

Reason

If you can't do a good job at recovering, at least you can get out before too much consequential damage is done.

See also Simulating RAII.

Note

If you cannot be systematic about error handling, consider "crashing" as a response to any error that cannot be handled locally. That is, if you cannot recover from an error in the context of the function that detected it, call abort(), quick_exit(), or a similar function that will trigger some sort of system restart.

In systems where you have lots of processes and/or lots of computers, you need to expect and handle fatal crashes anyway, say from hardware failures. In such cases, "crashing" is simply leaving error handling to the next level of the system.

Example
void do_something(int n)
{
       // ...
       p = static_cast<X*>(malloc(n,X));
       if (p==nullptr) abort();     // abort if memory is exhausted
       // ...
 }

Most systems cannot handle memory exhaustion gracefully anyway. This is roughly equivalent to

void do_something(Int n)
{
       // ...
       p = new X[n];    // throw if memory is exhausted (by default, terminate)
       // ...
 }

Typically, it is a good idea to log the reason for the "crash" before exiting.

Enforcement

Awkward

E.27: If you can't throw exceptions, use error codes systematically

Reason

Systematic use of any error-handling strategy minimizes the chance of forgetting to handle an error.

See also Simulating RAII.

Note

There are several issues to be addressed:

  • how do you transmit an error indicator from out of a function?
  • how do you release all resources from a function before doing an error exit?
  • What do you use as an error indicator?

In general, returning an error indicator implies returning two values: The result and an error indicator. The error indicator can be part of the object, e.g. an object can have a valid() indicator or a pair of values can be returned.

Example
Gadget make_gadget(int n)
{
    // ...
}

void user()
{
    Gadget g = make_gadget(17);
    if (!g.valid()) {
            // error handling
    }
    // ...
}

This approach fits with simulated RAII resource management. The valid() function could return an error_indicator (e.g. a member of an error_indicator enumeration).

Example

What if we cannot or do not want to modify the Gadget type? In that case, we must return a pair of values. For example:

std::pair<Gadget,error_indicator> make_gadget(int n)
{
    // ...
}

void user()
{
    auto r = make_gadget(17);
    if (!r.second) {
            // error handling
    }
    Gadget& g = r.first; 
    // ...
}

As shown, std::pair is a possible return type. Some people prefer a specific type. For example:

Gval make_gadget(int n)
{
    // ...
}

void user()
{
    auto r = make_gadget(17);
    if (!r.err) {
            // error handling
    }
    Gadget& g = r.val; 
    // ...
}

One reason to prefer a specific return type is to have names for its members, rather than the somewhat cryptic first and second and to avoid confusion with other uses of std::pair.

Example

In general, you must clean up before an eror exit. This can be messy:

std::pair<int,error_indicator> user()
{
    Gadget g1 = make_gadget(17);
    if (!g1.valid()) {
            return {0,g1_error};
    }

    Gadget g2 = make_gadget(17);
    if (!g2.valid()) {
            cleanup(g1);
            return {0,g2_error};
    }

    // ...

    if (all_foobar(g1,g2)) {
        cleanup(g1);
        cleanup(g2);
        return {0,foobar_error};
    // ...

    cleanup(g1);
    cleanup(g2);
    return {res,0};
}

Simulating RAII can be non-trivial, especially in functions with multiple resources and multiple possible errors. A not uncommon technique is to gather cleanup at the end of the function to avoid repetittion:

std::pair<int,error_indicator> user()
{
    error_indicator err = 0;

    Gadget g1 = make_gadget(17);
    if (!g1.valid()) {
            err = g2_error;
            goto exit;
    }

    Gadget g2 = make_gadget(17);
    if (!g2.valid()) {
            err = g2_error;
            goto exit;
    }

    if (all_foobar(g1,g2)) {
        err = foobar_error;
        goto exit;
    }
    // ...

exit:
if (g1.valid()) cleanup(g1); if (g1.valid()) cleanup(g2); return {res,err}; }

The larger the function, the more tempting this technique becomes. Aso, the larger the program becomes the harder it is to apply an error-indicator-based error handling strategy systematically.

We prefer exception-based error handling and recommend keeping functions short.

See also: Discussion.

Enforcement

Awkward.

E.28: Avoid error handling based on global state (e.g. errno)

Reason

Global state is hard to manage and it is easy to forget to check it. When did you last test the return value of printf()?

See also Simulating RAII.

Example, bad
???
Note

C-stye error handling is based on the global variable errno, so it is essentially impossible to avoid this style completely.

Enforcement

Awkward.