How I handle errors in Rust

5 minute read 2019-07-09 Ashkan Kiani -

The Answer

derive_more is a crate which has many proc macros, amongst which is a macro for deriving From for structs, enums, and newtypes. From is the basic mechanism for using ? ergonomically in a function which returns Result<T, Error>.

Almost everything I write has the derive_more crate as a dependency, and the following pattern:

use derive_more::From;

// I always name my error type `Error`
// #[non_exhaustive] // One day
#[derive(From, Debug)]
pub enum Error {
  //
  // EXTERNAL ERRORS
  //
  Io(std::io::Error),
  // I fully qualify errors to avoid name conflicts, and,
  // generally, only need to specify them once: here.
  Pg(postgres::Error),

  //
  // MY ERRORS
  //
  InvalidToken,

  // These can have data as well
  ThingNotFound {
    name: String,
  },

  // Rather than using a string, I prefer to use a type.
  YourInputIsBadAndIDontLikeIt,

  // You could use this to return strings if you wanted
  // I don't prefer this, myself.
  GenericFallback(&'static str),

  // Allows you to add future errors without breaking compatibility
  // for your user's `match` arms.
  //
  // This will eventually be done with #[non_exhaustive]
  // See the tracking issue permalink at the bottom and the
  // inspiration for this in burntsushi's reddit comment
  #[doc(hidden)]
  __Nonexhaustive
}

pub type Result<T> = std::result::Result<T, Error>;

fn main() -> Result<()> {
  let mut stdout = std::io::stdout();
  write!(stdout, "{}", "hello")?;

  write!(stdout, "hi").map_err(|_| Error::ThingNotFound)?;

  Ok(())
}

With custom displays

use derive_more::{From, Display};

#[derive(From, Display, Debug)]
pub enum Error {
  // External errors

  // This already derives Display
  Io(std::io::Error),

  // I could override it though
  #[display(fmt="postgres error")]
  Pg(postgres::Error),

  // My errors

  #[display(fmt="invalid token: {}", c)]
  InvalidToken {
    c: char
  },

  #[display(fmt="your input is bad and i dont like it")]
  YourInputIsBadAndIDontLikeIt,

  #[display(fmt="error: {}", _0)]
  GenericFallback(&'static str),
}

The Question

History

Rust has gone through many transformations since I first started following it with regards to error handling.

In the beginning there was Result<T, Box<error::Error>> from the built-in error crate, and no try! macro. We had to manually return our errors, but if your error type implemented Debug + Display, you would get Into<Box<Error>> for free.

Then came the try! macro, and eventually the (controversial at the time) ? operator was introduced. This operator became the defacto way of using Result's, and it the From pattern where you wrap errors from multiple different sources and crates into a single Error type much more prominent.

This paved the way for utility crates: error_chain was first. I tried error_chain, but I could never get it to stil. I stuck to Box<Error> for a while.

Then failure came through and quickly rose in popularity. failure was basically error 2.0. It provided a new struct Error and trait Fail.

Error has a lot of From<T> implementations which you can see here. trait Fail and struct Error synergize such that if you implement Fail for your Errors, then you get the From for free.

It also provides some helper macros to construct errors from strings like bail!.

Here's a quote on how failure is intended to be used straight from the README.

As a general rule, library authors should create their own error types and implement Fail for them, whereas application authors should primarily deal with the Error type. There are exceptions to this rule, though, in both directions, and users should do whatever seems most appropriate to their situation.

Here my problem with failure: for failure to be optimally effective, everyone has to be using it. That's not to mention that it kind of discards information in the process of what the underlying error is.

For more discussion on failure, I recommend this reddit thread.

Why derive_more

It allows me to define wrappers for external errors much like error-chain does, and explicitly define which external errors I intend to deal with. I could wrap failure::Errors as well, if I wanted.

Or, I could simply use .map_err(|_| Error::ExternalError)? to merge all of these into an anonymous error if I don't want to allow my users (usually me) to be able to tell what the underlying error is.

This approach allows me to encapsulate the most information while not forcing my users to pick any particular approach, and the boilerplate is very small.

Speed round

  • Using failure actually does have a small performance hit.
  • .map_err(|_| Error::*)? is not too verbose and the most explicit one can be about the actual type of error.
  • If I really want something like bail!, I can construct my own quickly.
#[macro_export]
macro_rules! bail {
    ($e:expr) => {
        return Err($crate::Error::GenericFallback($e));
    };
    ($fmt:expr, $($arg:tt)+) => {
        return Err($crate::Error::GenericFallback(format!($fmt, $($arg)+)));
    };
}
  • The added size to my libraries and crates is very, very small. Smaller than failure.
  • It's got some other useful derive macros, like the Display one, and about 30 others:
    • From, Into, Constructor, Not, Neg, Add, Sub, BitAnd, BitOr, BitXor, Mul, Div, Rem, Shr, Shl, AddAssign, SubAssign, BitAndAssign, BitOrAssign, BitXorAssign, MulAssign, DivAssign, RemAssign, ShrAssign, ShlAssign, FromStr, Display, Binary, Octal, LowerHex, UpperHex, LowerExp, UpperExp, Pointer, Index, IndexMut, TryInto, Deref, DerefMut,

References and further reading


published in programming and tagged rust and errors