Exceptions vs expected: Let’s find a compromise

This isn’t the blog post I wanted to publish today, this is the blog post I had to publish.

Simon blogged about using ADTs for error handling, leading to an interesting reddit discussion. Then Vittorio wanted to share his thoughts on the matter, leading to an even bigger reddit discussion. Now I’d like to chime in and offer a reasonable solution.

It is the age-old question: return codes vs exceptions. But this time, return codes have gotten an upgrade: std::expected and similar types.

The problems with exceptions

As far as I can tell, people have the following issues with exceptions:

Besides any size/speed overhead, I’m not talking about that here.

Sure you can static_assert() that they are marked noexcept — but they may not be marked noexcept and still don’t throw! Or you can write a wrapper function that is marked noexcept that invokes it — but this leads to std::terminate.

The problems with ADTs

A type like std::expected is what’s known as an algebraic data type in the functional world. In fact, this is a common pattern there.

ADTs for error handling have the following complains:

Those are mainly taken from this reddit discussion.

Looking at the bigger picture

As with most things, the disadvantages are opposites: “exceptions are too implicit!” — “ADTs are too explicit!”

So let’s take a step back and look at the bigger picture. In particular: if you’re writing a library and have a function that may fail — how do you report the error?

I’m going to quote this answer from Tony van Eerd here, as he put it so well:

Here’s the thing: as a function author (and thus decider of how it will report errors), how am I suppose to know whether a caller will want to handle the error right away or pass it on?

For example:

> string read_file(string_view filename); // read file contents, return as a string
> ```
>
> Is file-not-found an exceptional error, or expected? If the filename is coming from the User (ie a File Open dialog), shouldn't the file picker always give you a valid file? But if you are reloading a project (ie a video editor, and the file is "some_movie.avi", which is part of your project) you shouldn't be too surprised if the file has gone missing.
>
> Or if the calling code is `string config = read_file("hard-coded-config.txt")`. That is probably a programmer error (or install error), and maybe shouldn't be handled by any code (not immediate, not higher up).
>
> etc.
>
> As a function author, I don't think using "the caller probably will handle this" is a valid way to choose an error strategy.

If you want to write a truly flexible API, you have to do both: exceptions and error return codes.
Because sometimes the error is "exceptional" and sometimes it isn't.

This is what the [C++17 filesystem library](http://en.cppreference.com/w/cpp/experimental/fs) did:

cpp void copy(const path& from, const path& to); // throws an exception on error void copy(const path& from, const path& to, error_code& ec); // sets error code


However, this leads to some code duplication or boilerplate that happens when you implement one in terms of the other.

> Quick question: Which one should you implement in terms of which?
> Answer will come later on.

So what are you supposed to do?

Do what others do.

In particular, take a look at different programming languages.
When I hear about a new programming language, I look at two things:

1. How does generic programming work?

2. How does error handling work?

Both are relatively hard problems and it is interesting to see how they can be solved if you're not limited to the C++ way.
So let's take a look how two other programming languages solve error handling: Swift and Rust.

> Disclaimer: I've never actually written a non-trivial program in those languages,
> so my description may not be accurate.

## Error handling in Swift

> I heavily recommend taking a look at Swift.
> It is designed by some ex-C++ people and has a lot of things C++ didn't manage to do.

Swift choose to use exceptions.

> If you're familiar with Swift, you can correctly claim that this is not entirely accurate.
> But I'm using this term here to prove a point.

However, they do not suffer any of the problems listed above (as least not as much as C++):

* They are explicit:
  In Swift, when you have a function that may throw an exception, you have to specify the function as `throw`:

swift func canThrowErrors() throws -> String

func cannotThrowErrors() -> String


  But unlike `noexcept`, this is statically enforced.

  In addition, when *invoking* a function that may throw an exception, you have to make it clear as well:

swift result = try canThrowErrors(); result2 = cannotThrowErrors();


  This makes it immediately obvious which functions can throw exceptions and which can't.

* They are not difficult to use correctly:
  Sure, you still have to worry about exception safety but there are no implicit requirements on your code: they're made clear.  
  And as `throws` is part of the type system, Swift protocols - basically C++0x concepts - handle them as well.
  If you don't allow a certain protocol to provide a throwing function, you may not pass it a type that has a throwing function.
  In addition, `defer` allows guaranteed cleanup without the boilerplate of RAII.

* They are (somewhat) composable:
  In addition to calling a function with `try`, you can also call it with `try?`: This will convert it into an optional,
  which can be composed.
  There is also `try!` that terminates the program if the call threw an exception.

## Error handling in Rust

Rust, on the other hand, decided to use ADTs for error handling.
In particular, `Result<T, E>` — either result value `T` or error `E` — is used.

They've also managed to solve most of the problems I listed:

* They are ergonomic:
  A common pattern when dealing with ADTs is this one:

cpp result = foo(); if (!result) return result.error(); // do something with result.value()


  This pattern is so common, Rust provided a boilerplate solution:
  

rust // old way result = try!(foo());

// new built-in language feature result = foo()?;


  This does the same as the code above: early return with an error or continue otherwise.
  
  In addition, Rust also provides the function style facilities and proper pattern matching.

* They must not be ignored:
  `Result` is marked with a special attribute so the compiler will complain if the return value is simply discarded.

## Combining both worlds

What's interesting here is that both Swift and Rust error handling are very similar:
The main difference is the way the error is transported over the call stack.

And this means that both approaches are great solutions for specific situations:
Exceptions still have a runtime overhead when thrown, so they shouldn't be used for non-exceptional cases.
Whereas return values and branches have a small overhead when not thrown, so they shouldn't be used for rare errors.

However, if you're writing a widely usable library only the caller knows whether or not a situation is non-exceptional!

So we need a way to report errors, that:

* is implicit but not completely hidden away
* is explicit but not too verbose
* flexible enough to be used in all kinds of situations
* fully part of the type system so it can be checked with concepts
* cannot be ignored

If we want something that is fully part of the type system *right now*, without changing the language,
we have to put the error information in the return type.

> "What about constructors?", you might ask.
> Don't worry, [I've got you covered](/blog/2017/01/09/exceptions-constructor.html).

But this has an additional benefit:
Converting a return value into an exception can be done without any overhead:
The only cost is an additional branch for the `if (result.error()) throw error;`, but the function that produces the result will probably have a branch already.
If the call to the function is inlined, the extra branch can be eliminated.

So we need a new return type: Let's call it `result<T, E>`.
Much like `std::expected` or Rust's `Result` it either contains the "normal" return value `T` or some error information `E`.
And unlike `std::expected` it not only has the optional-like interface but also the monadic error handling routines (`map`, `and_then` etc).
People who want to use functional ADTs are happy already.

In order to please the exception fans, let's also provide a function `value_or_throw()` it either returns the `T` or converts `E` into some exceptions and throws that.
If you want to handle failure using exceptions, all you need is type a few characters after the function.

And if failure is a programming mistake, just call `value()` without any checks.
If an error occurred, this can lead to a debug assertion as it should.

But what if the error is ignored?

C++17 added `[[nodiscard]]`, which is [great](http://www.bfilipek.com/2017/11/nodiscard.html) but can easily be suppressed.
I propose something like an `assert(!unhandled_error)` in the destructor of `result` that terminates the program,
if you destroy a result without handling the error.
That way you must not forget handling it or call something explicit like `.ignore_error()`.

This solves all problems when invoking a single library function in your program.
However, it doesn't solve the problems of the library that needs to compose multiple results or writing generic code.
Dealing with `result` is still more verbose than exceptions:

cpp result calculate_bar() { auto first_result = calculate_foo1(); if (!first_result) return first_result.error();

auto second_result = calculate_foo2(first_result.value()); if (!second_result) return second_result.error();

return bar(second_result.value()); }


However, this can be solved with a small language addition - [operator `try`](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0779r0.pdf).
It is Rust's `try!` or `?` and makes it perfect:

cpp result calculate_bar() { auto first_result = try calculate_foo1(); auto second_result = try calculate_foo2(first_result); return bar(second_result); } ```

Conclusion

Error handling is difficult.

But I truly think that something like the result I’ve discussed combined with some form of try operator can be the solution to the problem.

Of course, I’ve glossed over many details and important design decisions:

There are many different implementations of this result for this reason: proposed std::expected has the basic things already, (Boost.)Outcome is another. I suggest you take a look at them, the authors spend a lot more time thinking about the problem than I just did.

Of course, if you’re simply writing application code you can use whichever one you like. However, if you’re writing a general purpose library, consider adopting these techniques.

Note that this way of error handling isn’t usable for all kinds of errors. An example would be out of memory. For that you should rather use the exception handler technique I’ve described here.

If you've liked this blog post, consider supporting me - a dollar a month can really help me out.

This blog post was written for my old blog design and ported over. If there are any issues, please let me know.