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_errness 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)
//     }
// }

yes, yes... let the trait flow through you

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!