Building Your Own Error Type: Part 1
I know this blog post looks long but I swear its mostly snippets of rustc
errors that you can just skim past.
Introduction
Yesterday I had a friend on twitter ask for help creating an error type for a library that acted like anyhow but was suitable to expose as part of a library API. I tossed something together based on my experience writing error handling libraries. Then another mutual pointed out that they were amazed by the code snippet I’d posted and asked if I could write a blog post about it. So here I am.
I’ve decided to structure this explanation by going over the problem I was trying to solve, and how I built up the solution. The techniques here aren’t new or anything, the overall design is very similar to std::io::Error
and the error kind pattern described in The Failure Book, with a little hint of trait fun that I learned from working with the source code in anyhow
in the process of writing eyre
. I just applied the design patterns that are common in the ecosystem to this particular problem in the best way I could think of. Most importantly, I don’t want anyone coming away from this post thinking this is the best way to write an error type for a library, and that you should all copy this pattern. My goal is to get everyone comfortable mixing and matching design patterns to get an error type that matches their needs and best fits in to the rest of their software architecture.
The Problem and Plan
First let me summarize the needs that Kat described for their theoretical error type. It needed a programmatic interface suitable for a library. I interpreted this to mean that it needed an enum that could easily be matched upon to handle specific kinds of errors. It needed the ability to capture backtraces, and it needed the ability to add contextual stack traces to the error, which I interpreted to mean they wanted to add new error messages to the error without necessarily changing the error’s kind that you would match against, something like the .wrap_err
method on eyre
or the .context
method on anyhow
.
I formed a vague plan, I knew for the API I’d want to make an Error type and a Kind type, where the error was a struct with private internal members, and the Kind was a non_exhaustive
enum. Getting the backtrace in there is pretty easy, just add a member for it to the outer Error
type and capture it whenever an Error
is constructed. The last feature is where I had to get a little creative, my plan was to make a separate ErrorMessage
type that could be constructed from arbitrary impl Display
types, and which optionally stored another ErrorMessage
to act as the source to grab the previous error message whenever you add a new error message. I imagined an API like this:
fn parse_config() -> Result<(), Error> {
Err(Kind::NoFile).wrap_err("config is invalid")?
}
Where when you print this error with an error reporter like anyhow
or eyre
you’d get:
Error:
0: config is invalid
1: file not found
Backtrace:
...
Or something along those lines.
Programmatic API - aka Reacting To Specific Errors
I started by sketching out the API and the basic types. First with just the handling bit because it seemed easiest. I split it into a lib.rs
file and another examples/report.rs
file to easily run the “test” to visually inspect the output and make sure that all the APIs worked together as expected:
lib.rs
:
#![allow(clippy::try_err)]
pub fn foo_handle() -> Result<(), Error> {
Err(Kind::Important)?
}
pub fn foo_no_handle() -> Result<(), Error> {
Err(Kind::NotImportant)?
}
pub struct Error {
kind: Kind,
}
#[non_exhaustive]
pub enum Kind {
Important,
NotImportant,
}
examples/report.rs
:
fn main() -> Result<(), eyre::Report> {
match playground::foo_no_handle() {
Ok(_) => {}
Err(e) if e.kind() == playground::Kind::Important => Err(e)?,
Err(_) => {}
}
match playground::foo_handle() {
Ok(_) => {}
Err(e) if e.kind() == playground::Kind::Important => Err(e)?,
Err(_) => {}
}
Ok(())
}
Now that I’ve got that setup here’s where I like to let rustc
take the driver’s seat so lets see what it says.
error[E0277]: `?` couldn't convert the error to `Error` --> src/lib.rs:8:28 | 7 | pub fn foo_no_handle() -> Result<(), Error> { | ----------------- expected `Error` because of this 8 | Err(Kind::NotImportant)? | ^ the trait `From<Kind>` is not implemented for `Error` | = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait = note: required by `from` error: aborting due to 2 previous errors
Alright, lets go ahead and add that rq:
impl From<Kind> for Error {
fn from(kind: Kind) -> Self {
Self { kind }
}
}
rustc
:
❯ cargo check --example report warning: field is never read: `kind` --> src/lib.rs:12:5 | 12 | kind: Kind, | ^^^^^^^^^^ | = note: `#[warn(dead_code)]` on by default warning: 1 warning emitted Checking playground v0.1.0 (/home/jlusby/playground) error[E0599]: no method named `kind` found for struct `playground::Error` in the current scope --> examples/report.rs:4:21 | 4 | Err(e) if e.kind() == playground::Kind::Important => Err(e)?, | ^^^^ private field, not a method
Alright, lets go silence that dead code warning rq and then add the kind method next:
#![allow(dead_code)]
impl Error {
pub fn kind(&self) -> Kind {
self.kind
}
}
rustc
:
❯ cargo check --example report Checking playground v0.1.0 (/home/jlusby/playground) error[E0507]: cannot move out of `self.kind` which is behind a shared reference --> src/lib.rs:18:9 | 18 | self.kind | ^^^^^^^^^ move occurs because `self.kind` has type `Kind`, which does not implement the `Copy` trait error: aborting due to previous error
Oops, forgot to impl copy, lets go add that rq, and Debug
while we’re at it:
#[derive(Debug, Copy, Clone)]
#[non_exhaustive]
pub enum Kind {
Important,
NotImportant,
}
rustc
:
❯ cargo check --example report Checking playground v0.1.0 (/home/jlusby/playground) error[E0369]: binary operation `==` cannot be applied to type `Kind` --> examples/report.rs:4:28 | 4 | Err(e) if e.kind() == playground::Kind::Important => Err(e)?, | -------- ^^ --------------------------- Kind | | | Kind error[E0277]: the trait bound `playground::Error: std::error::Error` is not satisfied --> examples/report.rs:4:68 | 4 | Err(e) if e.kind() == playground::Kind::Important => Err(e)?, | ^ the trait `std::error::Error` is not implemented for `playground::Error` | = note: required because of the requirements on the impl of `From<playground::Error>` for `Report` = note: required by `from`
Looks like it’s time to add some more traits to Kind
and Error
, if memory serves you only need PartialEq
to use ==
, though I’m surprised the error message here doesn’t indicate that:
// struct Error { ... }
impl std::error::Error for Error {}
#[derive(Debug, Copy, Clone, PartialEq)]
#[non_exhaustive]
pub enum Kind {
Important,
NotImportant,
}
At this point I know rustc is gonna tell me to add Debug
and Display
impls to Error
so I’ll just go ahead and do that myself. In this case however I know that I’m going to change this later, but for now to keep the example easier for a blog post I’m just gonna use the Display
impl on Kind
as the source of the Display
impl on Error
, which I can later change around when I add my .wrap_err
function.
use std::fmt;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.kind.fmt(f)
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Kind::*;
match self {
Important => write!(f, "this error was important to us"),
NotImportant => write!(f, "this error was not a place of honor"),
}
}
}
Now rustc
should be happy, lets see what it says.
rustc
:
❯ cargo check --example report warning: returning an `Err(_)` with the `?` operator --> examples/report.rs:4:62 | 4 | Err(e) if e.kind() == playground::Kind::Important => Err(e)?, | ^^^^^^^ help: try this: `return Err(e.into())` | = note: `#[warn(clippy::try_err)]` on by default = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#try_err warning: returning an `Err(_)` with the `?` operator --> examples/report.rs:10:62 | 10 | Err(e) if e.kind() == playground::Kind::Important => Err(e)?, | ^^^^^^^ help: try this: `return Err(e.into())` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#try_err warning: 2 warnings emitted Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Me: I really need to disable this warning in clippy, ugh
#![allow(clippy::try_err)]
Okay, it should work now, lets see our initial report format:
cargo
:
❯ cargo run --example report Compiling playground v0.1.0 (/home/jlusby/playground) Finished dev [unoptimized + debuginfo] target(s) in 0.23s Running `target/debug/examples/report` Error: this error was important to us Location: examples/report.rs:10:68
Beautiful, alright so now we’ve got our initial error type setup. We compile, we show nice error messages, we create it conveniently from a Kind
and the ?
operator, and we have an outer struct
type to work with to start adding our new features. Lets move onto backtrace next.
Backtrace
Here I’ll start by just slapping a Backtrace
in the struct and let rustc remind me what features to enable. In this example I’m going to use std::backtrace::Backtrace
because it’s easier to work with and is compatible with eyre
and anyhow
by default. I could make this work with backtrace::Backtrace
from the backtrace-rs crate but its a bit more involved so I’ll leave that to a later post if people are interested. Hopefully it won’t be needed however because backtrace stabilization is starting to move forward again ^_^
use std::backtrace::Backtrace;
#[derive(Debug)]
pub struct Error {
kind: Kind,
backtrace: Backtrace,
}
Alright rustc
, gimmi your worst:
rustc
:
❯ cargo check --example report Checking playground v0.1.0 (/home/jlusby/playground) error[E0658]: use of unstable library feature 'backtrace' --> src/lib.rs:4:5 | 4 | use std::backtrace::Backtrace; | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: see issue #53487 <https://github.com/rust-lang/rust/issues/53487> for more information = help: add `#![feature(backtrace)]` to the crate attributes to enable error[E0658]: use of unstable library feature 'backtrace' --> src/lib.rs:18:16 | 18 | backtrace: Backtrace, | ^^^^^^^^^ | = note: see issue #53487 <https://github.com/rust-lang/rust/issues/53487> for more information = help: add `#![feature(backtrace)]` to the crate attributes to enable error[E0063]: missing field `backtrace` in initializer of `Error` --> src/lib.rs:37:9 | 37 | Self { kind } | ^^^^ missing `backtrace` error: aborting due to 3 previous errors Some errors have detailed explanations: E0063, E0658. For more information about an error, try `rustc --explain E0063`. error: could not compile `playground`
Alright so lets enable that feature, I already know I gotta switch to nightly so I’ll start using that too:
#![feature(backtrace)]
impl From<Kind> for Error {
fn from(kind: Kind) -> Self {
Self {
kind,
// and we gotta actually capture it when we construct an `Error`
backtrace: Backtrace::capture(),
}
}
}
At this point rustc was immediately happy, so let’s go straight into running it:
cargo
:
❯ RUST_LIB_BACKTRACE=1 cargo +nightly run --example report Finished dev [unoptimized + debuginfo] target(s) in 0.01s Running `target/debug/examples/report` Error: this error was important to us Location: examples/report.rs:12:68 Stack backtrace: 0: eyre::DefaultHandler::default_with at /home/jlusby/.cargo/registry/src/github.com-1ecc6299db9ec823/eyre-0.6.1/src/lib.rs:689 1: core::ops::function::Fn::call at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:70 2: eyre::capture_handler at /home/jlusby/.cargo/registry/src/github.com-1ecc6299db9ec823/eyre-0.6.1/src/lib.rs:551 3: eyre::error::<impl eyre::Report>::from_std at /home/jlusby/.cargo/registry/src/github.com-1ecc6299db9ec823/eyre-0.6.1/src/error.rs:87 4: eyre::error::<impl core::convert::From<E> for eyre::Report>::from at /home/jlusby/.cargo/registry/src/github.com-1ecc6299db9ec823/eyre-0.6.1/src/error.rs:461 5: report::main at ./examples/report.rs:12 6: core::ops::function::FnOnce::call_once at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227 7: std::sys_common::backtrace::__rust_begin_short_backtrace at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:137 8: std::rt::lang_start:: at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:66 9: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/core/src/ops/function.rs:259 std::panicking::try::do_call at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/panicking.rs:381 std::panicking::try at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/panicking.rs:345 std::panic::catch_unwind at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/panic.rs:382 std::rt::lang_start_internal at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/rt.rs:51 10: std::rt::lang_start at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:65 11: main 12: __libc_start_main 13: _start
looks at backtrace hmm, something’s not right here, oh shit, I forgot to return the backtrace type via the Error
trait, okay lets go back and add that.
impl std::error::Error for Error {
fn backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}
}
cargo
:
❯ RUST_LIB_BACKTRACE=1 cargo +nightly run --example report Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/examples/report` Error: this error was important to us Location: examples/report.rs:12:68 Stack backtrace: 0: <playground::Error as core::convert::From<playground::Kind>>::from at ./src/lib.rs:44 1: playground::foo_handle at ./src/lib.rs:9 2: report::main at ./examples/report.rs:10 3: core::ops::function::FnOnce::call_once at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:227 4: std::sys_common::backtrace::__rust_begin_short_backtrace at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys_common/backtrace.rs:137 5: std::rt::lang_start:: at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:66 6: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/core/src/ops/function.rs:259 std::panicking::try::do_call at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/panicking.rs:381 std::panicking::try at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/panicking.rs:345 std::panic::catch_unwind at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/panic.rs:382 std::rt::lang_start_internal at /rustc/3525087ada7018ef227b10846648660b7f07b6d1/library/std/src/rt.rs:51 7: std::rt::lang_start at /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/rt.rs:65 8: main 9: __libc_start_main 10: _start
There we go, perfect. We can see the error being constructed from the Kind
in the playground
library I’m using to write this example, looks like we’re done here. Now comes the fun part, adding new error messages to the chain of errors.
Context Stack Traces aka Wrapping Errors Internally
Here I’m gonna start much like I did before, I’ll sketch out the new API and fill it in based on compiler errors. I tweek the example throwing functions to add the extra method call I want to make. In this case however I know that the new method I’m adding will not be added as an inherent method on Kind
or Error
, in order to use it through Result
the same way you can with eyre
and anyhow
you need to do it as an extension trait. I’ll go ahead and define that trait as well.
Disclaimer, the trait I’m going to define here is something that I first learned about from anyhow
’s source and it took me months to get good enough with traits to figure out how to write it as cleanly as this. Even then when I was first writing this example I had to go look up the source code for the Section
trait in color-eyre
to remember exactly how it’s supposed to be done.
pub fn foo_handle() -> Result<(), Error> {
Err(Kind::Important).wrap_err("this error should not be ignored")?
}
pub fn foo_no_handle() -> Result<(), Error> {
Err(Kind::NotImportant).wrap_err("it's okay to ignore this error")?
}
trait WrapErr {
type Return;
fn wrap_err<D>(self, msg: D) -> Self::Return
where
D: fmt::Display + Send + Sync + 'static;
}
❯ cargo +nightly run --example report Compiling playground v0.1.0 (/home/jlusby/playground) error[E0599]: no method named `wrap_err` found for enum `std::result::Result<_, Kind>` in the current scope --> src/lib.rs:9:26 | 9 | Err(Kind::Important).wrap_err("this error should not be ignored")? | ^^^^^^^^ help: there is an associated function with a similar name: `map_err` | = help: items from traits can only be used if the trait is implemented and in scope note: `WrapErr` defines an item `wrap_err`, perhaps you need to implement it --> src/lib.rs:67:1 | 67 | trait WrapErr { | ^^^^^^^^^^^^^
So good so far. The cool thing that this trait in eyre
and friends does is convert the inner error type too an eyre::Report
immediately before adding the new error message to the report. However, going straight for this feature and implementing WrapErr
for the generic set <E: Error>
will get you into trouble, because rustc
won’t be able to be sure that Result<...>
can’t also be turned into your Error
type.
The key here is to split it into two impls, the first impl, directly on your Error
type, just handles adding the context, then your second impl and beyond all handle just the conversions and map_err
ness or w/e. Here’s what it ends up looking like:
impl WrapErr for Error {
type Return = Error;
fn wrap_err<D>(self, msg: D) -> Self::Return
where
D: fmt::Display + Send + Sync + 'static,
{
todo!()
}
}
impl<T, E> WrapErr for Result<T, E>
where
E: Into<Error>,
{
type Return = Result<T, Error>;
fn wrap_err<D>(self, msg: D) -> Self::Return
where
D: fmt::Display + Send + Sync + 'static,
{
todo!()
}
}
For now I’m going to leave this unimplemented, because we haven’t yet created the spot to store our error messages, rustc seems pretty happy with us tho, the only complaint is about the unused msg
argument.
The next step is we need a way to store an ErrorMessage
, and a previous error message, such that we can create a linked list of sources internally without losing access to our Kind
or Backtrace
.
We start with a definition for the error message struct that looks like a singly linked list:
struct ErrorMessage {
message: Box<dyn fmt::Display + Send + Sync + 'static>,
source: Option<Box<ErrorMessage>>,
}
Add this to our Error
type:
#[derive(Debug)]
pub struct Error {
kind: Kind,
backtrace: Backtrace,
message: ErrorMessage,
}
And adjust our Display
impl on Error
to use the new message member instead of kind:
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
I’m already sure this will anger rustc
so lets see where they tell us to start next:
rustc
:
❯ cargo +nightly check --example report Checking playground v0.1.0 (/home/jlusby/playground) error[E0277]: `ErrorMessage` doesn't implement `Debug` --> src/lib.rs:20:5 | 20 | message: ErrorMessage, | ^^^^^^^^^^^^^^^^^^^^^ `ErrorMessage` cannot be formatted using `{:?}` | = help: the trait `Debug` is not implemented for `ErrorMessage` = note: add `#[derive(Debug)]` or manually implement `Debug` = note: required because of the requirements on the impl of `Debug` for `&ErrorMessage` = note: required for the cast to the object type `dyn Debug` = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error[E0599]: no method named `fmt` found for struct `ErrorMessage` in the current scope --> src/lib.rs:25:22 | 25 | self.message.fmt(f) | ^^^ method not found in `ErrorMessage` ... 101 | struct ErrorMessage { | ------------------- method `fmt` not found for this | ::: /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/mod.rs:930:8 | 930 | fn fmt(&self, f: &mut Formatter<'_>) -> Result; | --- | | | the method is available for `Box<ErrorMessage>` here | the method is available for `Arc<ErrorMessage>` here | the method is available for `Rc<ErrorMessage>` here | = help: items from traits can only be used if the trait is implemented and in scope = note: the following traits define an item `fmt`, perhaps you need to implement one of them: candidate #1: `Debug` candidate #2: `std::fmt::Display` candidate #3: `Octal` candidate #4: `Binary` candidate #5: `LowerHex` candidate #6: `UpperHex` candidate #7: `Pointer` candidate #8: `LowerExp` candidate #9: `UpperExp` error[E0063]: missing field `message` in initializer of `Error` --> src/lib.rs:43:9 | 43 | Self { | ^^^^ missing `message` error: aborting due to 3 previous errors Some errors have detailed explanations: E0063, E0277, E0599. For more information about an error, try `rustc --explain E0063`. error: could not compile `playground`
Whew, big error message, lets go add those impls rq:
impl From<Kind> for Error {
fn from(kind: Kind) -> Self {
Self {
kind,
backtrace: Backtrace::capture(),
message: ErrorMessage::from(kind),
}
}
}
#[derive(Debug)]
struct ErrorMessage {
message: Box<dyn fmt::Display + Send + Sync + 'static>,
source: Option<Box<ErrorMessage>>,
}
impl fmt::Display for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
Here you’ll notice im using the kind
itself to create the initial error message, since it’s copy. This seemed like a reasonable default to me, but feel free to take a different approach if you prefer.
rustc
:
❯ cargo +nightly check --example report Checking playground v0.1.0 (/home/jlusby/playground) error[E0308]: mismatched types --> src/lib.rs:46:41 | 46 | message: ErrorMessage::from(kind), | ^^^^ expected struct `ErrorMessage`, found enum `Kind` error[E0277]: `(dyn std::fmt::Display + Send + Sync + 'static)` doesn't implement `Debug` --> src/lib.rs:104:5 | 104 | message: Box<dyn fmt::Display + Send + Sync + 'static>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn std::fmt::Display + Send + Sync + 'static)` cannot be formatted using `{:?}` because it doesn't implement `Debug` | = help: the trait `Debug` is not implemented for `(dyn std::fmt::Display + Send + Sync + 'static)` = note: required because of the requirements on the impl of `Debug` for `Box<(dyn std::fmt::Display + Send + Sync + 'static)>` = note: required because of the requirements on the impl of `Debug` for `&Box<(dyn std::fmt::Display + Send + Sync + 'static)>` = note: required for the cast to the object type `dyn Debug` = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) error: aborting due to 2 previous errors
Oh god damnit, you can’t derive Debug
with a dyn Display
type, thats annoying, okay w/e:
struct ErrorMessage {
message: Box<dyn fmt::Display + Send + Sync + 'static>,
source: Option<Box<ErrorMessage>>,
}
impl<D> From<D> for ErrorMessage
where
D: fmt::Display + Send + Sync + 'static,
{
fn from(message: D) -> Self {
ErrorMessage {
message: Box::new(message),
source: None,
}
}
}
impl fmt::Debug for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
Here I decided to go with a from impl for all Display
types so that we can easily make error messages from whatever, I have secret plans for this later:
rustc
:
❯ cargo +nightly check --example report Checking playground v0.1.0 (/home/jlusby/playground) error[E0119]: conflicting implementations of trait `std::convert::From<ErrorMessage>` for type `ErrorMessage`: --> src/lib.rs:107:1 | 107 | / impl<D> From<D> for ErrorMessage 108 | | where 109 | | D: fmt::Display + Send + Sync + 'static, 110 | | { ... | 116 | | } 117 | | } | |_^ | = note: conflicting implementation in crate `core`: - impl<T> From<T> for T; error: aborting due to previous error
Oh god damnit, fking overlap rule, this says that it the fact that ErrorMessage
impls Display
means we can’t impl From<Display>
because then ErrorMessage::from(error_message)
wouldn’t know if it should do an identity conversion (just immediately return the same value) or wrap it inside the message field of a new ErrorMessage
.
…, Fuck it! We don’t need to impl Display
here, its an internal type, and we already have an identical Debug
impl, buahahahaha.
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Debug;
self.message.fmt(f)
}
}
// impl fmt::Display for ErrorMessage {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// self.message.fmt(f)
// }
// }
cargo
:
❯ cargo +nightly run --example report Compiling playground v0.1.0 (/home/jlusby/playground) warning: unused variable: `msg` --> src/lib.rs:81:26 | 81 | fn wrap_err<D>(self, msg: D) -> Self::Return | ^^^ help: if this is intentional, prefix it with an underscore: `_msg` | = note: `#[warn(unused_variables)]` on by default warning: unused variable: `msg` --> src/lib.rs:95:26 | 95 | fn wrap_err<D>(self, msg: D) -> Self::Return | ^^^ help: if this is intentional, prefix it with an underscore: `_msg` warning: 2 warnings emitted Finished dev [unoptimized + debuginfo] target(s) in 0.37s Running `target/debug/examples/report` thread 'main' panicked at 'not yet implemented', src/lib.rs:99:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Sick, we’re in the final stretch, just gotta fill in the WrapErr
impls and I think we’re good to go, this one will be a little tricky, but it should be fine with our handy dandy friend std::mem::swap
.
impl WrapErr for Error {
type Return = Error;
fn wrap_err<D>(mut self, msg: D) -> Self::Return
where
D: fmt::Display + Send + Sync + 'static,
{
let mut message = ErrorMessage {
message: Box::new(msg),
source: None,
};
std::mem::swap(&mut self.message, &mut message);
self.message.source = Some(Box::new(message));
self
}
}
impl<T, E> WrapErr for Result<T, E>
where
E: Into<Error>,
{
type Return = Result<T, Error>;
fn wrap_err<D>(self, msg: D) -> Self::Return
where
D: fmt::Display + Send + Sync + 'static,
{
self.map_err(|e| {
let e = e.into();
e.wrap_err(msg)
})
}
}
… Is that it? I think we’re done :O
Lets see what we got
cargo
:
❯ cargo +nightly run --example report Compiling playground v0.1.0 (/home/jlusby/playground) Finished dev [unoptimized + debuginfo] target(s) in 0.30s Running `target/debug/examples/report` Error: this error should not be ignored Location: examples/report.rs:12:68
Hmm, nope, we’re missing something… Oh yea, we forgot to return our source in the Error
trait, I swear I always do this…
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.message.source()
}
fn backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}
}
rustc
:
❯ cargo +nightly run --example report Compiling playground v0.1.0 (/home/jlusby/playground) error[E0599]: no method named `source` found for struct `ErrorMessage` in the current scope --> src/lib.rs:32:22 | 32 | self.message.source() | ^^^^^^-- help: remove the arguments | | | field, not a method ... 119 | struct ErrorMessage { | ------------------- method `source` not found for this | = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `source`, perhaps you need to implement it: candidate #1: `std::error::Error` error: aborting due to previous error
Oh yea, gotta impl Error
on ErrorMessage
if I want to be able to make the previous error messages available in the chain of source
errors, awe shit I know where this is going…
impl std::error::Error for ErrorMessage {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_ref().map(|e| e as _)
}
}
Sigh, okay rustc, let me have it:
rustc
:
❯ cargo +nightly run --example report Compiling playground v0.1.0 (/home/jlusby/playground) error[E0277]: `ErrorMessage` doesn't implement `std::fmt::Display` --> src/lib.rs:136:6 | 136 | impl std::error::Error for ErrorMessage { | ^^^^^^^^^^^^^^^^^ `ErrorMessage` cannot be formatted with the default formatter | ::: /home/jlusby/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/error.rs:48:26 | 48 | pub trait Error: Debug + Display { | ------- required by this bound in `std::error::Error` | = help: the trait `std::fmt::Display` is not implemented for `ErrorMessage` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Sad face, bitten in the ass by my own cleverness. I guess I’ll just manually impl From
for Kind only :(((
impl From<Kind> for ErrorMessage {
fn from(kind: Kind) -> Self {
ErrorMessage {
message: Box::new(kind),
source: None,
}
}
}
impl std::error::Error for ErrorMessage {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_ref().map(|e| e as _)
}
}
impl fmt::Debug for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
impl fmt::Display for ErrorMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.message.fmt(f)
}
}
Okay, this time I think we got it…
❯ cargo +nightly run --example report Compiling playground v0.1.0 (/home/jlusby/playground) Finished dev [unoptimized + debuginfo] target(s) in 0.35s Running `target/debug/examples/report` Error: this error should not be ignored Caused by: this error was important to us Location: examples/report.rs:12:68
Fuck yea! Nailed it, I’m the best ^_^
Conclusion
Okay so that’s how I write error types, I ended up writing a recreation of the dev process by literally just doing it again from scratch. I hope you all enjoy reading it as much as I enjoyed half assing writing it.
Extras
There are a few features here that could be added easily that I didn’t go ahead and do in this case. For example, eyre
and anyhow
both occupy only one pointer on the stack, where as this Error
kind is rather chonky in comparison, with the multiple Boxes in ErrorMessage
and the Backtrace
. It’s not much harder to make the Error
type defined here into a private ErrorImpl
and then make a new struct Error(Box<ErrorImpl>);
type to wrap it. This will help with happy path code by keeping stack frames smaller, but adds an extra allocation when errors are actually thrown, its up to you to decide which you need more.
Another thing, in this example I only ever showed creating Errors
from Kind
, but you could add other From
impls for Error
. Say you have a lot of random errors that aren’t important to handle, you could add something like this:
impl From<&'static str> for Error {
fn from(msg: &'static str) -> Error {
Error {
kind: Kind::Other,
backtrace: Backtrace::capture(),
message: ErrorMessage::from(msg),
}
}
}
Get creative with it, you’ll probably run into some of the same fmt::Display
overlap errors here so you’ll have to implement the from impls for each individual type you want to use as a source, which you can deduplicate with macros if you end up doing with many different types.
Thats all I’ve got for now. If you’ve got a similar problem where you don’t know how to write just the right error handling type for your library or application please feel free to nerd snipe me. I’ll love to do this again and get a Part 2 up, and so on. Take care everyone!