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
union
s and casts) - Resource leaks (including memory leaks)
- Bounds errors
- Lifetime errors (e.g., accessing an object after is has been
delete
d) - 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
- E.2: Throw an exception to signal that a function can't perform its assigned task
- E.3: Use exceptions for error handling only
- E.4: Design your error-handling strategy around invariants
- E.5: Let a constructor establish an invariant, and throw if it cannot
- E.6: Use RAII to prevent leaks
- E.7: State your preconditions
E.12: Use
noexcept
when exiting a function because of athrow
is impossible or unacceptable- E.13: Never throw while being the direct owner of an object
- E.14: Use purpose-designed user-defined types as exceptions (not built-in types)
- E.15: Catch exceptions from a hierarchy by reference
- E.16: Destructors, deallocation, and
swap
must never fail - E.17: Don't try to catch every exception in every function
- E.18: Minimize the use of explicit
try
/catch
E.19: Use a
final_action
object to express cleanup if no suitable resource handle is availableE.25: If you can't throw exceptions, simulate RAII for resource management
- E.26: If you can't throw exceptions, consider failing fast
- E.27: If you can't throw exceptions, use error codes systematically
- E.28: Avoid error handling based on global state (e.g.
errno
)
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 string
s constructors may not be able to allocate sufficient memory for their elements, vector
s constructor may not be able copy the Thing
s 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 throw
s 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 swap
s 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.