quam serena

Using derive_more for errors in Rust

Errors can be very annoying in Rust. This is not necessarily a bad thing, though, because unlike other languages, Rust forces you to actually think about errors at compile time rather than be surprised at run time. Let's look at an example function, which might return an error:

#[derive(Debug)]
struct FooError;

fn foo() -> Result<(), FooError> {
    ...
}

We'll say that FooError contains some error information data which is unique to foo(). And now let's say the same for another function, bar():

#[derive(Debug)]
struct BarError;

fn bar() -> Result<(), BarError> {
    ...
}

In practice, these might be two different dependencies that each define their own Error type. Now, let's say that we have a function foobar() which must call both:

fn foobar() -> Result<(), ????> {
    foo()?;
    bar()?;
}

We have a problem here. The line foo()? will try to return Err(FooError) if it encounters an error, but bar()? would return Err(BarError) which are not the same type. So what do we put as the error type? A simple solution is to create an enum and use .map_error:

#[derive(Debug)]
enum FoobarError {
    FooError(FooError),
    BarError(BarError)
}

fn foobar() -> Result<(), FoobarError> {
    foo().map_error(FoobarError::FooError)?;
    bar().map_error(FoobarError::BarError)?;
}

This works nicely, but now our function becomes a bit verbose. With functions with a lot of Results, this can get unwieldy very quickly. Luckily, ? performs an implicit .into() on the error type, so if we implement From on FoobarError we can remove the .map_errors:

#[derive(Debug)]
enum FoobarError {
    FooError(FooError),
    BarError(BarError)
}

impl From<FooError> for FoobarError {
    fn from(value: FooError) -> Self {
        Self::FooError(value)
    }
}

impl From<BarError> for FoobarError {
    fn from(value: BarError) -> Self {
        Self::BarError(value)
    }
}

fn foobar() -> Result<(), FoobarError> {
    foo()?;
    bar()?;
}

This works nicely, but the From<...> impl's are a bit annoying to write. It would be nice to automatically derive them — and that's where the crate derive_more comes in:

#[derive(Debug, From)]
enum FoobarError {
    FooError(FooError),
    BarError(BarError)
}

fn foobar() -> Result<(), FoobarError> {
    foo()?;
    bar()?;
}

This all works in no_std and did so even before error-in-core was stabilized. It's a good idea to also derive the Error trait, which derive_more also supports. You can also derive the Display trait and use thiserror-like syntax, giving you a great amount of flexibility with no boilerplate. I typically define one error enum for the entire crate, mark it pub, define an alias of Result using it, and then use that everywhere.

Comparison to other approaches

Usually, when confronted with this problem, application authors reach for crates like anyhow and library authors for something like thiserror. derive_more was directly inspired by thiserror; it can do everything that thiserror does plus more. Up until somewhat recently, thiserror didn't support no_std, leading many projects like Bevy to switch to derive_more. I'm in the same boat with my projects, having already chosen derive_more due to lack of support on the thiserror side. It is somewhat a matter of personal opinion the choice between the two crates; derive_more having more functionality and being better maintained tips my scales in its favor for me.

anyhow is different than thiserror and derive_more as it wraps all errors in one opaque type instead of an enum and is intended to only really show the error message. Since matching on the error type is cumbersome (it can be done through downcasting, though it is not as easy as a simple match statement), it isn't a great choice for libraries. But sometimes I've also wanted to match on an error inside application code, in which case anyhow still isn't a good fit. Compare using anyhow!(...) with adding an extra enum variant: anyhow! is one line, and using an extra enum variant is... three lines, if I'm counting correctly, with the added benefit of having no dependency on any library in the function signature. For this reason I have stopped using anyhow altogether. Anyhow also requires an allocator, making it not suitable for many embedded applications. Having tried a multitude of error handling crates, derive_more has given me the best experience in terms of no_std support and avoidance of boilerplate.