Error Handling in Rust, Part 2: Composability
This article presents a second part of my series on Rust error handling that I started in a post three months ago. Now we'll build on that foundation to explore composability – how to effectively combine and manage errors in more complex Rust applications
Let's recall the simple example of making espresso in a coffee machine from the first part:
Composing to Make Breakfast
Composition is a fundamental tool in programming and allows us to solve huge problems by breaking them up into small puzzles that can be cracked individually. It is hard for any intelligence - human or artificial - to reason about a complex system by observing all its moving parts. It helps a great deal to form abstractions and build the solution layer by layer: from the small cogs to movements and on to the whole clockwork. To show how this abstract framework can be applied to error handling in Rust, let's consider our coffee machine a part of a larger system designed to make breakfast:
Here the task of preparing breakfast is explicitly broken down into parts. Just as we want
to compose the whole breakfast out of toast and espresso, we want to compose
the error reported from the make_breakfast function from information provided by any of the inner
functions representing individual steps. To do this, we match on the return value of make_espresso
and when get a string from the error variant, we form a new string stating what went wrong with
the inner error string appended. This is illustrated by the following diagram

This approach allows us to propagate information about causes which is useful if it affects how the error is handled for example.
Building Composable Error Types
In the above example we used String as the error type and composed it manually like this:
Err => Err,
Now this would get very tedious in a real world codebase. We only used it to demonstrate the concept
and will now look for better alternatives. What types should we use to propagate error information?
To start our search, let's peek inside the standard library and see if there is any guideline
on defining error types. The first thing we see is the following
std::error::Error
trait declaration (I omitted the deprecated methods):
The standard library says that this trait defines basic expectations for error values. They
must describe themselves through Debug and Display traits and may provide cause information
through Error::source(). To take a closer look, here is how the function is described
in the documentation:
Error::source()is generally used when errors cross “abstraction boundaries”. If one module must report an error that is caused by an error from a lower-level module, it can allow accessing that error viaError::source(). This makes it possible for the high-level module to provide its own errors while also revealing some of the implementation for debugging.
The reason why we digged specifically into this function is because we will discuss the potential
for breaching encapsulation when propagating cause information later in the article. At the
bottom of the documentation page for the Error trait, you are going to see that the
trait is implemented for many error types in the standard library:
impl Error for std::fmt::Error
impl Error for std::io::Error
(...)
So should you also define your own error type and implement the std::error:Error trait yourself?
Well, there are at least two other options:
- Use a crate like
thiserrorto auto-implementstd::error:Error - Adopt a general-purpose error type from a crate like
anyhow
There is no right strategy, you should pick what suits your code. Let's now look at the first
option and some specifics of the thiserror crate.
Fanning out with thiserror
Pulling in the thiserror crate, we can rewrite the make_espresso function from our first
example as follows:
use Error;
The key feature of thiserror
is the thiserror::Error derive macro which is commonly applied to enums defining the
possible error variants, but can be applied to structs and other kinds as well. The error
helper attribute
is used to provide accompanying error messages on which the automatic Display implementation is based.
To see how composability can be achieved with thiserror, we can rewrite the breakfast example like
this:
Here we leverage the fact that Rust enum variants can carry data and therefore we can propagate the
source error directly. But in the above example, there is no explicit construction
of the UnableToMakeEspresso variant and it still works. But how? Part of the magic is achieved
by the ? operator acting on the Result. Under the hood, it searches for a way to convert
the possible error value from the result into the error value of the function in whose body
it is being used. But MakeBreakFastError and MakeEspressoError are different types and their
values cannot be converted between by default. That is where the from helper attribute comes
into play since it generates a From<MakeEspressoError> for MakeBreakFastError implementation. Plus
the formatting in the "Unable to make espresso, {0}" string achieves the same composition effect
on the error messages we achieved manually in the first part of this article.
To see how the technique works in practice, consider the following test:
It's execution produces this output:
Unable to make espresso, Not enough coffee beans
There is one important consequence of propagating the source errors by plugging them in
as data or members in the outer error type. If inner type is also public for some reason, it could
be referenced by callers directly as part of their error handling code and this presents a risk
of encapsulation breach. See a strategy based
on the transparent helper attribute proposed by the thiserror crate documentation for an idea
how to deal with this challenge.
Folding in with anyhow
The approach taken by the anyhow crate is quite different from thiserror and a great deal
simpler. The core of it lies in using anyhow::Result in all fallible functions you define
yourself:
use ;
If you are wondering why the anyhow::Result has just one type parameter then you have found wherein
lies the simplicity: it is essentially just a typedef to a standard Result where the error type
is anyhow::Error. The following implementation is associated with anyhow::Error:
So any error type satisfying core::Error + Send + Sync + 'static can be converted into
anyhow::Error. Hopefully it will be better explained with an example of anyhow in action
when making breakfast:
Running the test below demonstrates how we achieve the same compositional effect on error
messages with anyhow::Context:
Unable to make espresso
Not enough coffee beans
The different nature of anyhow and thiserror
The following diagram illustrates the fold in property of anyhow::Error which can absorb
information from almost any type implementing core::Error and contrasts it with thiserror:

As opposed to anyhow, thiserror has a fan out effect. When applied to an enum type, the
variants can represent different causes of failure of a specific function. The caller can then
choose to act in different ways on the particular errors.
Thanks to its fold in nature, anyhow is well suited for use in application code. E.g. in
some core loop of a service or application where a number of functions are executed over and
over. As breaking such loop to execute some specific error handling and recovery routines
is not usually desired on most failures, a possible error chain can be
folded into anyhow::Error discarding the majority of the information carried by the source error types.
Then, an anyhow::Error returned by any of the loop parts will mostly just be logged and there will
be no specific code trying to figure out the cause.
On the other hand, thiserror shines where the caller should be able to easily react
to a particular cause to do some specific error handling.
Summary
- Think about composability in error types, is it useful to you?
- Use
anyhowas a quick start anyhowis mostly suitable for application code- If caller needs to match on different causes, use
thiserror - Use
thiserrorin libraries, but you might also consider defining your own type