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 Result
s, 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_error
s:
#[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.