Skip to content

Joseph Mansfield

Exceptions, error codes, and assertions in C++

It can often be difficult to decide between the various methods of error reporting in C++. For example, some common advice is that exceptions should only be thrown in exceptional circumstances. Needless to say, this isn't particularly helpful. What exactly is an exceptional circumstance? An exception to what? If we throw assertions into the mix, this can become even more complicated.

In general, functions express a contract to the calling code. They promise to perform some operation and give a particular result according to the given inputs. A member function can be considered to take an additional parameter, this, which is the object it's acting on. When a member function is not const, its this parameter is also an output. Error reporting is how we handle when there is a problem with meeting this contract and ensure that the relevant parties have been informed.

Error reporting can occur at compile-time, in which case the compiler emits the error, or at run-time. Detecting a problem at compile-time is usually much better, as the program simply will not build until it is fixed and there will be no run-time cost. However, run-time error reporting is necessary for problems that cannot be statically detected or for when the error can be considered part of the normal execution of the program as long as it is handled appropriately.

Function design

The ultimate way to prevent misuse of a function is to design its interface so that it simply cannot be misused. This means choosing parameter and return types that accurately represent what the user should be able to pass to and receive from the function. For example, raw pointers often do not sufficiently express the function's intent and there are almost always better alternatives. It also means ensuring that your functions behave responsibly - no hidden side effects, no hidden dependencies, proper exception safety, and so on. With later versions of C++, it will also mean using concepts (draft N4040) appropriately to constrain template arguments.

The more expressive your function interfaces are with built in language features, the more the user can trust your functions and be confident that they will not screw anything up.

Static assertions

The next best thing is to introduce your own static error checking with static_assert. Using type traits and constant expressions, we can form conditions that can be evaluated and checked at compile-time. For example, while we do not yet have concepts, it is easy to enforce that a template function should only be instantiated for an integral type:

template <typename T>
void foo(T arg) {
  static_assert(std::is_integral<T>::value, "T must be an integral type");
  // ...
}

Static assertions can only evaluate constant expressions, so you won't be able to check anything that isn't known until run-time. For example, if you receive input from the user, you cannot check the value of this input with a static_assert. For that you need run-time error reporting.

Exceptions

When a problem cannot be detected at compile-time or when an error should reported to the calling code, the above approaches are not appropriate. Instead, we look to exceptions, error codes, and run-time assertions.

Exceptions can sometimes be quite difficult to decide when to use. What exactly constitute an exceptional circumstance? I prefer to think of it this way: a function should throw an exception if it is unable to fulfil the promise that it made to the calling code. To figure out what that promise is, summarize your function in one simple sentence (with no conjunctions). What does the function do? Look at the name of the function, the parameters it takes, and the objects it returns and write down what it promises. If you can't do this, perhaps your function should be simplified or split up into multiple functions.

Once you have a simple description of the function's behavior, it's easy to determine when an exception should be thrown. If it can't fulfil its promise, it should throw. It might be because the user gave it some arguments that were unsuitable or because some internal problem occurs - either way, throw an exception. As an example, consider the following function declaration:

image load_image(boost::filesystem::path image_file_path);

It should be clear what this function promises to do. It says "I will load an image from the given file path." If it is unable to perform this job, perhaps because the file does not exist or there is some other I/O problem, it should throw an exception. In fact, what else would it do in these situations? If it returns, it has to return an image object after all. You could have a special value of image that represents that an error has occurred, but I strongly advise against that. Exceptions allow the function to break out early and not have to care about returning an object of the correct type.

The only issue with exceptions is that you have to be careful. With the lack of useful exception specifications in C++, it is important to check the documentation for a function to see if it throws. Always try to write exception safe code.

A common fallacy is that exceptions are slow. In fact, modern exception implementations have no cost when the exception is not thrown (unlike an explicit check with if). There is some cost when throwing an exception, but you should find that they are not thrown often, so this cost can largely be ignored. Regardless, favor code readability and safety over performance until you measure that exceptions are a bottleneck in your code.

Error codes

Error codes have much less reason to be used in modern C++. They come in two flavors: an output object dedicated to expressing error conditions, or a special value of an output object that represents the error condition. Output parameters (that is, non-const reference or pointer parameters) are not typically a good idea and return objects should be preferred. This means that with the first flavor, the error codes must either be the sole output of a function or returned as part of a std::tuple or std::pair. This may be an acceptable use of error codes. However, using a special value is not likely to be acceptable. In the above example, an image that failed to load should not be a valid image object. Doing so introduces special cases to your code and ruins any uniformity.

The important thing about error codes is that, unlike exceptions, they are part of the promise that a function makes. If the simple description of your function states that it provides error codes as part of its normal functionality, then an error code is okay. An obvious example for this is a function that is used specifically to report errors from some state (such as glGetError). In this way, an error code shouldn't really represent a problem with the operation of the function, since returning an error code is actually successful operation.

Some gray areas arise when we look at functions that really do have a special case result. Consider a function like std::find that tries to find an element in a container. If it doesn't find the element, is that an exception? I would suggest it is not because the item not existing is a perfectly reasonable response to being asked to find it. Isn't the past-the-end iterator returned by std::find in this case an example of a special return value? Perhaps, but it provides a certain uniformity that allows it to be used in conjuction with other algorithms. An alternative interface might return a boost::optional with an optional iterator (in C++17, std::optional).

Run-time assertions

The assert macro from the <cassert> header, also known as a C assert, is able to check run-time conditions and abort execution if the condition is not met. It doesn't report the problem to the calling function. Since your program immediately ends when these assertions fail, they probably shouldn't make it into production builds. In fact, assert is defined such that you can turn it off by defining the macro name NDEBUG, so that it will only be triggered in debug builds.

Due to these properties, assert is most useful for sanity checks. That is, use assert internally to ensure that your code is doing what you expect it to do. It means "I know that this should be true in all cases, so if it's not, something is terribly wrong." For example, if you write a function that performs some complex calculation and you know that the result should always be greater than a certain value, otherwise the calculation has been implemented incorrectly, use assert to check this. Then when you run your debug build and a call to this function occurs that causes the assertion to fail, you'll know immediately that there's a problem and can start debugging the issue.

Summary

In summary, the different forms of error reporting should be used as follows:

Static assertions
To prevent invalid instantiations of templates and to check other compile-time conditions.
Exceptions
To let some calling code know that a function was unable to fulfil its contract due to some run-time problems.
Error codes
To report run-time conditions that are part of a function's contract and considered normal behavior.
Run-time assertions
To perform sanity checks on internal operations at run-time and ensure that major bugs do not enter production builds.

Remember that carefully designing your functions can avoid the need for many of these checks by preventing the user from misusing them in the first place.