Dealing with Errors in C++ Using a Lightweight Monadic Approach

dark vintage technology blur

Managing errors and failures in every programming language is usually a pain. Most programming book authors just show the happy path scenario, sometimes noting down that error handling has to be done, but it has been left out for improving simplicity (and readability).

C++ offers the exception mechanism, which is a clever way to leave the happy path in sight and hide the troubles under the carpet. Even before questioning if this is a good idea or not, C++ abstraction is so delicate that you need to take particular care in making your code exception-safe. Meaning that in case of exception your program does not leak resources and leaves everything in a useful state so that the exception can indeed be recovered from.

RAII and smart pointers are part of the correct exception handling toolset from the design and coding point of view. More generally you need to be able to afford some extra memory (for storing stack unwinding tables) and some non-deterministic execution time.

Also, you may be confronted with the dilemma of what is an exception and what is an error. Trying to open a file that does not exist? They tell me that this is not an exception because it is a possible outcome of file opening… I am afraid these “distinguo”s are too subtle and thus controversial.

Functional programming way of error handling is via Either<L,R>, that is functions always return something, and that something can either be a value or an error. This is a very good abstraction, but eventually, you end up with everything wrapped into an Either. That could be annoying and in C++ even more, since the language lacks monadic composition (such as the for-comprehension we have in other languages).

Moreover, when working with firmware and embedded systems you have many functions that can return an error, but no real value when succeeding. Yes, these are impure functions, which are relevant just because of their side effects. Maybe someday we could use an effect system in C++ (a la Cats Effects or Zio), but today C++ (and C++ community) is not ready yet.

What you see in many firmware or embedded software projects is a sequence like this:

int result = 0;
result = initStuff();
if( result != 0 )
{
    return result;
}
result = sendData();
if( result != 0 )
{
    return result;
}
result = waitReady();
return result;

Not nice to write, not a joy for the eyes, possibly subjected to copy’n’paste antipattern. Screaming “compose me” to any functional programmer.

After tinkering with alternatives for a while, I came up with this solution I named (with a brilliant effort of creativity) Error. Error is just 4 bytes of data, partitioned like error code (8 bits), error module (8 bits), and additional data (16 bits). An error code of 0 means no error regardless of the module where the error occurred. All other codes are defined within the module context. Additional data is an optional field defined according to the module and error code that may contain additional data that can be useful when reporting or debugging the error.

And this is nothing special. Maybe some clever way to let module self register, but pretty traditional C/C++ code.

Things get more interesting by adding the andThen method:

    template<typename F>
    inline Error
    Error::andThen( F f ) const noexcept( std::is_nothrow_invocable_v<F> )
    {
        return m_code == 0 ?
            f() :
            *this;
    }

In other words, function f (which must return an Error) is invoked only if this Error object contains no error. Otherwise f is not called and this object is returned.

Now you can rewrite the code above in a more elegant and smart way:

return initStuff()
    .andThen( [](){ return sendData(); } )
    .andThen( [](){ return waitReady(); } );

Once you get over the ugly lambda syntax that the committee gave us, the sequence is quite trivial. In case of error, the sequence is interrupted, no more calls are performed and the error value is returned to the caller.

The Error behavior is pretty similar to Either<E,void> where E is the error code. When the Either is successful the result value has no information, when the Either is a failure then you have the error code.

If the andThen() method looks pretty familiar the reason is that it is indeed a flatMap.

So, am I inventing the wheel or just reinventing a monad? Let’s see. In order to be a monad, we need two methods: pure and bind. Pure is the constructor and we know how to construct an Error, be it successful or failing.

Bind is the flatMap operation, Error embeds information (ok/error code) that can be transformed via a function producing another Error.

And then we need to follow the monad laws: right identity, left identity, and associativity.

Before diving into laws, we need to define equality between two Errors. When two Errors are both ok, the data field is not used and the source module is not really interesting – the call has succeeded, how does it matter where?

When the Error represents a failure then all fields are relevant since I want them to guide me in finding the problem (possibly not any problem, but the first problem that caused the error to happen.

Back to laws. Right identity laws prescribe there exists an element I such that e.andThen(I) is equal to e. Ok value is such an element I, because if e is a failure, then the result is e, if e is OK, then the result is still OK.

The same holds for left identity, where OK is once again the identity element.

Associativity is a bit trickier since you need to investigate three items: a, b and c and verify that:

(a.andThen( b )).andThen(c) = a.(andThen( b ).andThen(c))

Let’s demonstrate it with a table of truth (possibly there are more elegant ways to demonstrate it):

abctruth
OkOkOktrue
OkOkErrortrue
OkErrorOktrue
OkErrorErrortrue
ErrorOkOktrue
ErrorOkErrortrue
ErrorErrorOktrue
ErrorErrorErrortrue

Qed (it’s from a long time I wished to write QED ).

Well, that may seem like a fruitless exercise in some mathetmaticism with no practical purpose or application.

Quite the opposite. First having a solid math foundation “you can prove stuff” (as my good friend Sandro once told me) and you make sure that you can compose and use Error with the standard FP constructs. Also, it is straightforward to lift Error to other kinds of monads.

Now, before starting coding with Error claiming to be a fully functional programmer, a bit of caution is due.

Functional programming preaches that functions must be pure, i.e. function just computes something starting from its arguments, and its arguments only, and only produces a result. No side effects, no global state access. Error has no return value, therefore functions returning an Error must do something in the environment to be useful. In fact, this kind of function is usually employed for initializing hardware peripherals. From this point of view is pretty impure.

Also, consider associativity, if you just look at the result, then things are fine, but since the order of invocation may matter, associativity does not work in general.

That is my Error is as close to a Monad as possible for an imperative code, without being a Monad.

You can find Error implementation in my chef library.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: