Flexible error handling techniques in C++
Sometimes things aren’t working. The user enters stuff in the wrong format, a file isn’t found, a network connection fails and the system runs out of memory. Those are errors and they need to be handling.
In a high-level function this is relatively easy. You know exactly why something was wrong and can handle it in the right way. But for low-level functions this isn’t quite as easy. They don’t know what was wrong, they only know that something was wrong and need to report it to their caller.
In C++ there are two main strategies: error return codes and exceptions. The “modern”, mainstream C++ way of handling errors are exceptions. But some people cannot use/think they cannot use/don’t want exceptions - for whatever reason.
This blog post isn’t going to pick a side on the fight. Instead I am describing techniques that make both sides - relatively - happy. Those techniques are especially useful if you are developing libraries.
Note: this post was originally designed as a stand-alone one. But because I later started a series about error handling, it make sense to make it a part of it. In terms of the “first” part of the series: It talks about choosing the right recoverable error handling technique for system errors or user errors deep in the call stack.
A sort of followup is now available here: Exceptions vs expected: Let’s find a compromise.
The problem
I’m working on foonathan/memory as you probably know by now. It provides various allocator classes so let’s consider the design of an allocation function as an example.
This is just about the error handling, not about the other design choices. For those you need to wait for my Meeting C++ talk in November. Looking forward to seeing you there.
For simplicity consider malloc()
.
It returns a pointer to the allocated memory.
But if it couldn’t allocate memory any more it returns nullptr
, eh NULL
,
i.e. an error value.
This has some disadvantages though: You need to check every call to malloc()
.
If you forget it, you use non-existing memory, which is bad™.
Also error codes are transitive by nature:
If you call a function that can return an error code and you can’t ignore it or handle it otherwise,
you must return an error code itself.
This leads to code where the normal code path and the error code path is interleaved. Exceptions can be seen as a better alternative. With exceptions you only need to handle the error if you care about it. Otherwise it will be silently passed back to the caller.
This can also be seen as a disadvantage to be fair.
And exceptions in those case also have a very big advantage: The allocation function either returns valid memory or not at all. It is a “do all or nothing” function, the return value will always be valid. According to Scott Meyer’s “Make interfaces hard to use incorrectly and easy to use correctly” this is a good thing.
So for those reasons it can be argued that you should use exceptions as error handling mechanism. And this is the opinion of most C++ developers, including me. But as a library that provides allocators it aims at real-time applications. For many developers of those applications - especially game programmers - using exceptions is an exception.
Yes, pun intended.
So to please those developers it would be best if my library doesn’t use exceptions. But I and some others like exceptions as an elegant and simple way of error handling, so to please those developers it would be best if my library does use exceptions.
So what am I supposed to do?
The ideal solution would be if you have the option to enable or disable exceptions as you wish. Those who like exceptions can use them, those who don’t don’t have to. But due to the nature of exceptions you cannot simply swap them with error codes because there will be no internal code that checks for those - after all the internal code relies on the transparent nature of exceptions. And even if it is possible to use error codes internally and translate to exceptions if needed, you lose much of the benefits of exceptions.
Luckily, I am in a special position because consider what you actually do when you encounter an out of memory error: Most of the time you log and abort the program because it can’t usually work properly without memory. Exceptions in these cases are simply a way to transfer control to another piece of code that does the logging and aborts. But there is an old and powerful way of doing such a transfer control: a function pointer, i.e. a handler function.
If you have exceptions enabled, you simply throw them. Otherwise you call a handler function and abort the program afterwards. The abort at the end is important because it prevents a do-nothing handler function that is intended to let the program continue as normal. This would be fatal because it violates the essential postcondition of the function: it will always return a valid pointer. Other code can rely on it, after all, it is normal behavior.
I call this technique exception handler and this is what I’ve used in memory.
Solution I: Exception handler
If you need to handle an error where the most common handling behavior is just “log-and-abort”,
you can use an exception handler.
An exception handler is a handler function that gets called instead of throwing the exception object.
It can be implemented quite easily, even in existing code by putting the handler management into the exception class and wrapping the throw
statement in a macro.
First, augment the exception class and add functions for setting and maybe querying a handler function.
I suggest you do that in a similar way the standard library handles std::new_handler
, i.e. like so:
class my_fatal_error
{
public:
// handler type, should take the same parameters as the constructor
// in order to allow the same information
using handler = void(*)( ... );
// exchanges the handler function
handler set_handler(handler h);
// returns the current handler
handler get_handler();
... // normal exception stuff
};
Because it is in the scope of the exception class you don’t need a special way of naming it. Naming is hard, so this is nice.
You can also use conditional compilation to remove the handler stuff if exceptions are enabled. If you like you can also write a generic mixin class that provides the required functionality.
The elegance is the exception constructor: it calls the current handler function passing it the required arguments from its parameters.
Then combine that with the following throw
macro:
#if EXCEPTIONS
#define THROW(Ex) throw (Ex)
#else
#define THROW(Ex) (Ex), std::abort()
#endif
Such a throw macro is also provided by foonathan/compatiblity.
You can use it like so:
THROW(my_fatal_error(...))
If you have exception support enabled this will create the exception object and throws it as usual.
But if you don’t have exception support enabled this will also create the exception object - and this is important - and only then calls std::abort()
.
And because the constructor calls the handler function it works as required:
You have a customization point for logging the error.
And because of the std::abort()
after the constructor, the user cannot undermine the post-condition.
In memory I also have the handler enabled if exceptions are enabled so it will be called even if an exception is thrown.
This technique allows a fallback if you don’t have exceptions enabled that still allows some form of customization. Of course it isn’t a perfect replacement: only for log-and-abort. You cannot continue after it. But in the situation of out-of-memory and some others this is a viable replacement.
But what if you want to continue after the exception?
The exception handler technique doesn’t allow that because of the post-condition of the code after that. So how do you enable this behavior?
The simple answer is: you can’t. At least not in such a simple way you can in the other case. You cannot just return an error code instead of an exception if the function isn’t designed for that.
There is only one viable option: Provide two functions; one that returns an error code and one that throws. Clients who want exceptions use the throwing variant, clients who don’t, the error code version.
Sorry if that is obvious. I’m only mentioning it for the sake of completeness.
As an example, take the memory allocation function again. In this case I would use the following functions:
void* try_malloc(..., int &error_code) noexcept;
void* malloc(...);
The first version returns nullptr
if the allocation fails and sets error_code
to the error code.
The second version never returns nullptr
but throws instead.
Note that it is very easy to implement the second version in terms of the first:
void* malloc(...)
{
auto error_code = 0;
auto res = try_malloc(..., error_code);
if (!res)
throw malloc_error(error_code);
return res;
}
Don’t do this the other way round, then you have to catch
the exception, which is expensive.
This would also prevent compiling without exception support.
If you do it as shown you can simply delete the other overload through conditional compilation.
And even if you have exception support enabled, the client still want the non-throwing version. An example would be if it needs to allocate the maximum size possible in this example. Calling it in a loop and checking with a conditional is simpler and faster than catching an exception to detect that.
Solution II: Provide two overloads
If an exception handler isn’t sufficient, you need to provide two overloads. One overload uses a return code, the other throws an exception.
If the function in question has a return value, you can simply use the return value to transport for the error code.
Otherwise you need to return an “invalid” value - like the nullptr
in the example above - to signal the error
and set an output parameter to the error code if you want to provide further information to the caller.
Please do not use a global
errno
variable or aGetLastError()
kind of facility!
If the return value doesn’t have an invalid value to indicate failure,
consider using std::optional
- once it is available for you - or similar.
The exception overload can - and should - be implemented in terms of the error code version as shown above. If you compile without exceptions you can erase this overload through conditional compilation.
This more work for you, but at least when implementing the exception overload, you can call the error code version internally and just translate.
std::system_error
This kind of system is perfect for the C++11 error codes facility.
It adds std::error_code
which is the non-portable error code, e.g. returned by OS functions.
Through a complicated system of library facilties and error categories you can add your own error codes or std::error_condition
s, which are portable versions.
Read an introduction about it here.
If appropriate, you can use std::error_code
in the error code function.
And for the exception function you have an appropriate exception class: std::system_error
.
It takes a std::error_code
and is used to report those errors as exceptions.
All low-level functions that are close wrappers of OS functions should use this facility or similar. It is a good - albeit complicated - replacement for the OS error code facility.
Yes, I still need to add those for memory’s virtual memory functions. They currently don’t provide an error code.
std::expected
As mentioned above there is an issue if you don’t have a return value that has an invalid value you can use to signal error. Furthermore the output parameter isn’t nice to get the error code.
And global variables aren’t an option for that!
N4109 proposes a solution: std::expected
.
It is a class template that either stores a return value or an error code.
In the example above it would be used like so:
std::expected<void*, std::error_code> try_malloc(...);
On success, std::expected
will store a non-null pointer to the memory and on failure it will store the std::error_code
.
This technique now works for any return value.
A pair of std::expected
+ exception functions will definitely allow any use case.
Conclusion
As library author you sometimes have to provide maximal flexibility for your clients. This includes error handling facilities: Sometimes error return codes are wanted, sometimes exceptions.
One strategy to accomodate those needs is an exception handler. Simply ensure that a callback is called instead of an exception thrown if needed. It is a replacement for fatal errors that will just be logged before termination anyway. As such it does not work everywhere and you cannot simply switch between both versions in the same program. This is just a workaround for disabled exception support.
A more flexible solution is if you simply provide two overloads one with exceptions and one without. Then the users has maximum freedom and can pick the version that is best suited for each situation. The downside is that you as library implementer have to do more work.
A sort of followup is now available here: Exceptions vs expected: Let’s find a compromise.
This blog post was written for my old blog design and ported over. If there are any issues, please let me know.