One of the simplest error handlers ever written

Author: Chloé Lourseyre
Editor: Peter Fordham

This week, I’ll present you a small device I wrote to handle basic errors, the most compact and generic I could think of.

It is certainly not perfect (mainly because perfection is subjective) but it is very light-weight and easy to use.

If you want to skip the article and go directly to the source, here is the Github repo: SenuaChloe/SimplestErrorHandler (github.com)

Specifications

In terms of error handling, my needs are usually as follow:

  • The error handler must write a message on the error output (std::cerr).
  • The error handler must be able to take several arguments and stream them into the error message.
  • The error handler must raise an exception.
  • The what() of the exception must return the same message that is prompted on std::cerr.
  • The specific type of exception raised must be configurable.
  • Raising an error must be a one-function call.
  • The error handler must not rely on macros.

So that’ll be the basic criteria I’ll be relying on to design the error handler (we may add a few specifications along the way).

Step 0: Setup

To make it very light and simple, all the code will be in a single header file (it’s simpler to include a header into your project than a lib). But since there will probably be auxiliary functions (that won’t be part of the interface), we need a way to hide them.

That’s why we will put all the code in a full-static class1. There will be private static member functions (for this internal functions), public static member functions (the interface), and possibly types and such.

Step 1: Basic recursion and variadic templates

Starting with a simple recursion

To have a fully customizable error message, we need a variable number of arguments (and thus some variadic templates). We will recurse over the arguments2, streaming each of them, starting with head, into a stream.

template<typename THead>
static void raise_error_recursion(const THead & arg_head)
{
    std::cerr << arg_head << std::endl;
    throw;
}

template<typename THead, typename ...TTail>
static void raise_error_recursion(const THead & arg_head, const TTail & ...arg_tail)
{
    std::cerr << arg_head;
    raise_error_recursion(arg_tail...);
}

The first raise_error_recursion represents the base condition of the recursion: if there is only one argument, then we print it then throw.

The second raise_error_recursion represents the recursion loop. As long as there is more than one argument in the arg_tail parameter pack, we call the second raise_error_recursion, which prints the arg_head into cerr and then calls itself back. As soon as there is only one parameter left in the parameter pack, we end up in the first overload that ends the recursion.

With a stream and a real exception

However, in the snippet just above, we don’t throw any exception, we just throw;. As a reminder, two of the specification were:

  • The error handler must raise an exception.
  • The what() of the exception must return the same message that is printed on std::cerr.

So we need to throw a real exception, and that exception must be constructed with our error message.

As an example, we’ll use the std::runtime_error exception, which can be constructed with a std::string.

The problem is then that we can’t just stream the error message into cerr anymore: we need a way to memorize the message to, at the end, stream it into cerr and construct our runtime_exception.

An solution to that is to add a stringstream as a parameter of the recursive functions.

template<typename THead>
static void raise_error_recursion(std::ostringstream & error_string_stream, const THead & arg_head)
{
    error_string_stream << arg_head;
    const std::string current_error_str = error_string_stream.str(); 

    std::cerr << current_error_str << std::endl;
    throw std::runtime_error(current_error_str);
}

template<typename THead, typename ...TTail>
static void raise_error_recursion(std::ostringstream & error_string_stream, const THead & arg_head, const TTail & ...arg_tail)
{
    error_string_stream << arg_head;
    raise_error_recursion(error_string_stream, arg_tail...);
}

There, in the body of the recursion, we stream the error message into the stringstream instead of cerr. In the base case of the recursion, we convert this stream into a string that is then used to construct the exception and to print the error output.

Is that it?

These two functions are the mainframe of the error handling. We don’t need anything more than that to fulfill most of the specifications.

But of course, it’d be great to add a few enhancements that’ll ease the use of the handler.

Step 2: Adding interface

This stringstream is a pain and should be invisible to the user. Thus, we’ll put the previous functions in the private part of the class and write a member function to be used as interface:

template<typename ...TArgs>
static void raise_error(const TArgs & ...args)
{
    std::ostringstream error_string_stream;
    raise_error_recursion(error_string_stream, args...);
}

raise_error is now a very simple function to use.

Step 3: Adding customizable exceptions

The exception as a template

The only spec that is not implemented is “The specific type exception raised must be configurable“.

To do that we will add a template to each function. This represents the exception that must be raised.

class ErrorHandler
{
    ErrorHandler(); // Private constructor -- this is a full-static class
    
    template<typename TExceptionType, typename THead>
    static void raise_error_recursion(std::ostringstream & error_string_stream, const THead & arg_head)
    {
        error_string_stream << arg_head;
        const std::string current_error_str = error_string_stream.str();

        std::cerr << current_error_str << std::endl;
        throw TExceptionType(current_error_str);
    }

    template<typename TExceptionType, typename THead, typename ...TTail>
    static void raise_error_recursion(std::ostringstream & error_string_stream, const THead & arg_head, const TTail & ...arg_tail)
    {
        error_string_stream << arg_head;
        raise_error_recursion<TExceptionType>(error_string_stream, arg_tail...);
    }

public:

    template<typename TExceptionType, typename ...TArgs>
    static void raise_error(const TArgs & ...args)
    {
        std::ostringstream error_string_stream;
        raise_error_recursion<TExceptionType>(error_string_stream, args...);
    }

    template<typename TExceptionType>
    static void raise_error()
    {
        raise_error<TExceptionType>("<Unknown error>");
    }
};

This way, you can call raise_error with any exception that is constructable with a std::string, like so:

ErrorHandler::raise_error<std::runtime_error>("Foo ", 42);

However, this is a bit heavy. Sometimes, you just wanna raise a generic error and don’t mind whether it is a runtime_error, an invalid_argument, etc.

That’s why we’ll add a default value for the template TException. Unfortunately, we can’t use std::exception for this default value because it can’t be constructed using a std::string.

What I suggest is to define our own generic exception, within the namespace of the ErrorHandler. This way, we’ll have a generic exception to be used as a default value, and users may use it as base class to implement custom exceptions, all related to error handling (which can be useful in try-catches).

A custom generic exception for the error handler

class BasicException : public std::exception
{
protected:
    std::string m_what;
public:
    BasicException(const std::string & what): m_what(what) {}
    BasicException(std::string && what): m_what(std::forward<std::string>(what)) {}
    const char * what() const noexcept override { return m_what.c_str(); };
};

Of course, there is a public inheritance of std::exception so that BasicException can be used like any other standard exception3.

I implemented two constructors, one that builds the error message using a constant reference string, and one that builds the error message using a r-value reference (to be able to move data into the constructor).

And, of course, the what() virtual overload that returns the error message.

Using this exception as default, the raise_error functions now look like this:

template<typename TExceptionType = BasicException, typename ...TArgs>
static void raise_error(const TArgs & ...args)
{
    std::ostringstream error_string_stream;
    raise_error_recursion<TExceptionType>(error_string_stream, args...);
}

template<typename TExceptionType = BasicException>
static void raise_error()
{
    raise_error<TExceptionType>("<Unknown error>");
}

Now you can raise an error without having to provide an exception:

ErrorHandler::raise_error("Foo ", 42);

This will throw a ErrorHandler::BasicException by default.

Step 4: Adding an assert one-liner

The most common situation when you have to raise an error is if <something is wrong> then <raise an error>. It can also be seen as assert <expression>, if false <raise an error>.

This is commonly encountered in unit-testing, functions that take the form of assert(expression, message_if_false);

That’s why I think it’s a good idea to add a single function that will take an expression and a parameter pack (the error message) and call raise_error if the expression is not true.

template<typename TExceptionType = BasicException, typename ...TArgs>
static void assert(bool predicate, const TArgs & ...args)
{
    if (!predicate)
        raise_error<TExceptionType>(args...);
}

Using this, instead of writing this:

bool result = compute_data(data);
if (result != ErroCode::NO_ERROR)
    ErrorHandler::raise_error("Error encountered while computing data. Error code is ", result);

You’ll be able to write something like this:

bool result = compute_data(data);
ErrorHandler::assert(result == ErroCode::NO_ERROR, "Error encountered while computing data. Error code is ", result);

Step 5: Concept and constraints

We use a lot of templates. Many templates mean that the user will be likely to misuse them. Leading to compilation errors. And when we talk about template-related compilation errors, we talk about almost illegible error messages.

But, lucky us, there is a way in C++20 to make these error more readable while protecting our functions better: concepts and constraints.

We currently have two constraints:

  • TExceptionType must with constructible using a std::string.
  • Every TArgs... must be streamable.

So we’ll implement these two constraints within a single concept4:

template<typename TExceptionType, typename ...TArgs>
concept ErrorHandlerTemplatedTypesConstraints = requires(std::string s, std::ostringstream oss, TArgs... args)
{
    TExceptionType(s); // TExceptionType must be constructible using a std::string
    (oss << ... << args); // All args must be streamable
};

We now only have to add this concept as a constraint on our interface member functions:

template<typename TExceptionType = BasicException, typename ...TArgs>
requires ErrorHandlerTemplatedTypesConstraints<TExceptionType, TArgs...>
static void raise_error(const TArgs & ...args)
{
    std::ostringstream error_string_stream;
    raise_error_recursion<TExceptionType>(error_string_stream, args...);
}

template<typename TExceptionType = BasicException, typename ...TArgs>
requires ErrorHandlerTemplatedTypesConstraints<TExceptionType, TArgs...>
static void assert(bool predicate, const TArgs & ...args)
{
    if (!predicate)
        raise_error<TExceptionType>(args...);
}

The complete code

If we put everything together, the resulting header file looks like this:

#pragma once

#include <iostream>
#include <sstream>

template<typename TExceptionType, typename ...TArgs>
concept ErrorHandlerTemplatedTypesConstraints = requires(std::string s, std::ostringstream oss, TArgs... args)
{
    TExceptionType(s); // TExceptionType must be constructible using a std::string
    (oss << ... << args); // All args must be streamable
};

class ErrorHandler
{
    ErrorHandler(); // Private constructor -- this is a full-static class
    
    template<typename TExceptionType, typename THead>
    static void raise_error_recursion(std::ostringstream & error_string_stream, const THead & arg_head)
    {
        error_string_stream << arg_head;
        const std::string current_error_str = error_string_stream.str();

        std::cerr << current_error_str << std::endl;
        throw TExceptionType(current_error_str);
    }

    template<typename TExceptionType, typename THead, typename ...TTail>
    static void raise_error_recursion(std::ostringstream & error_string_stream, const THead & arg_head, const TTail & ...arg_tail)
    {
        error_string_stream << arg_head;
        raise_error_recursion<TExceptionType>(error_string_stream, arg_tail...);
    }

public:

    class BasicException : public std::exception
    {
    protected:
        std::string m_what;
    public:
        BasicException(const std::string & what): m_what(what) {}
        BasicException(std::string && what): m_what(std::forward<std::string>(what)) {}
        const char * what() const noexcept override { return m_what.c_str(); };
    };

    template<typename TExceptionType = BasicException, typename ...TArgs>
    requires ErrorHandlerTemplatedTypesConstraints<TExceptionType, TArgs...>
    static void raise_error(const TArgs & ...args)
    {
        std::ostringstream error_string_stream;
        raise_error_recursion<TExceptionType>(error_string_stream, args...);
    }

    template<typename TExceptionType = BasicException, typename ...TArgs>
    requires ErrorHandlerTemplatedTypesConstraints<TExceptionType, TArgs...>
    static void assert(bool predicate, const TArgs & ...args)
    {
        if (!predicate)
            raise_error<TExceptionType>(args...);
    }
};

To go further

We could push the genericity of the handler a little further and try to replace the std::cerr output stream by a customizable output stream that takes std::cerr by default.

However, that would mean more functions, a longer code, and the goal is to keep the header as short as possible.

It’s up to you now to stop here or go further and complete the implementation.

Wrapping up

This is certainly not the most complete way to handle errors in your program, but this is, in my opinion, a simple and clean way to do it while fulfilling the established specifications.

Up to you now to define your own specifications and to write your own error handler if your needs are different than mine.

You can use this code (almost) as you wish, as it is under the CC0-1.0 License.

Thanks for reading and see you next week!

Author: Chloé Lourseyre
Editor: Peter Fordham

Addenda

Github repo

SenuaChloe/SimplestErrorHandler (github.com)

Useful documentation

I used a lot of advanced features of C++. To learn more about them, follow the links:

Notes

  1. “Full-static” means that the class won’t be instantiable. All its member functions and member variable will be static, and the constructor will be private. That’s why we need a class and can’t use a namespace here: with a namespace, we couldn’t hide any auxiliary function.
    If you don’t want to use a full-static class and still want to hide the auxiliary functions, you’d have to put them into a cpp file. But if you do this, you need to compile the error handler as a lib in order to import it in other projects.
  2. To learn about recursion, read this page: Recursion – GeeksforGeeks. To learn about variadic parameters and templates, here you go: Variadic arguments – cppreference.com and Parameter pack(since C++11) – cppreference.com.
  3. See std::exception – cppreference.com to have some insight into how exceptions work in C++.
  4. The only small problem with that is that we have to implement the concept in the global namespace. That is why I used a pretty long name that begins with “ErrorHandler”: to avoid name collision as much as possible.

3 thoughts on “One of the simplest error handlers ever written”

Leave a Reply