Slide from a talk by Jane Losare-Lusby, who maintains the eyre
crate
- Starting on a real world rust project, the first pattern thing-y I encountered pretty quickly was custom error handling and converting other errors into my error in functions returning a result.
- Pre-prerequisite: Repeat 3 times, giving it more thought every time: Errors in Rust are not Exceptions!!! They're values
- Prerequisite: Rust on its own provides 3 things for error handling
std::result::Result
std::error::Error
?
which is an operator that is equivalent to:match operand { Ok(x) => x, Err(e) => return Err(From::from(e)), }
- Theoretically anything that implements the
{rust} Try
trait can use this operator (which is sometimes called the{rust} Try
operator), but it's currently unstable, so we can only use it with{rust} Result
&{rust} Option
- Theoretically anything that implements the
- There are a lot of patterns here in rust, including:
- A custom
{rust} mycrate::Result<T>
type alias, such as:{rust} pub type Result<T> = std::result::Result<T, MyCrateError>;
to use as return type for functions without having to repeat the error - Providing
{rust} impl Error
for custom error types - Providing
{rust}impl <T: Error> From<T>
to allow for conversion of other errors to our error type when using the?
operator (As it calls{rust} From::from
in the{rust} Err
match arm) - Using enum variants per other such error: keeping our errors as sum types.
- In general also using enums etc. instead of strings in order to allow the user to get as much information as possible about the error.
- Rust by example section on this: Wrapping errors - Rust By Example
- A custom
- A lot of these patterns are easily implemented using the
thiserror
crate by David Tolnay- Also consider if using
derive_more
crate instead is better if only{rust} From
implementation is required and{rust} Display
is not required, e.g.: In a backend service, the{rust} Display
implementation is not likely to be used:{rust} Serialize
is much more likely to be used to just print the context variables as a JSON - For the
{rust} Display
part there is alsodisplaydoc
by the maintainer ofeyre
: it takes the message from doc comments instead of an attribute, achieving both documentation & printability while keeping them in sync as well.
- Also consider if using
anyhow
is another crate that acts as{rust} Box<dyn Error>
(but more performant with a custom v table implementation) replacement to allow using?
with any{rust} T: Error
in functions returning{rust} anyhow::Result
.However, it's probably not a good practice to use anyhow, as it's a way of shoving your work under the rug for later- Another notable crate is
eyre
, focusing on error reporting customizability. - Finally,
snafu
provides syntax sugar for{rust} map_err
by generating a convenient context propagation syntax
- What do I use then? Depends if it's a library or an application
- For library errors: We need maximum flexibility here, so it's not a good idea to use error reporters such as
anyhow
,eyre
orBox<dyn Error>
, as the don't compose well due to not implementingError
itself (due to Rust's overlap rule). So use error defining traits, such asthiserror
- Also make sure to use
non-exhaustive
in theError
enum being defined.
- Also make sure to use
- For application errors: Here we need reporting for arbitrary errors as they'll encounter errors from numerous sources. So
anyhow
,eyre
etc. work well here. - Note that these are just rules of thumb, specific situations might warrant a different approach.
- For library errors: We need maximum flexibility here, so it's not a good idea to use error reporters such as
- For any new project, add these two lines in
error.rs
and re-export in the beginning ofmain.rs
/lib.rs
:// In main.rs/lib.rs mod error; pub use self::error::{Error, Result}; // In error.rs pub type Result<T> = core::result::Result<T, Error>; pub type Error = Box<dyn std::error::Error>; // For early dev
- Eventually we want to move to something like this:
use derive_more::From; use core::{fmt::{self, Display, Formatter}, result}; pub type Result<T> = core::result::Result<T, Error>; #[derive(Debug, From)] pub enum Error { // -- When fs module doesn't have too many errors yet LimitTooHigh { actual: usize, max: usize }, // -- When fs module has more than 2-3 errors #[from] Fs(fs::Error), // -- Externals #[from] Io(std::io::Error), // as an example } // Note: Implement Display as debug, for Web and app error, as anyway // those errors will need to be streamed as JSON probably to be rendered // for the end user. impl Display for Error { fn fmt(&self, fmt: &mut Formatter) -> result::Result<(), fmt::Error> { write!(fmt, "{self:?}") } } impl std::error::Error for Error {}
- This video gives an intuition for how to think about Error Handling:
- This video gives an idea of what creating an error involves, and what boilerplate is required:
- This video sums up a lot of the patterns very well:
- This video goes over the best practices and patterns the Rust community is heading towards:
- Text version: Error Handling - Rust Best Practices
- A good article to go through before creating custom errors when creating a library crate for others to use: Study of std::io::Error
- Other articles:
- Error Handling In Rust - A Deep Dive | Luca Palmieri (Sample from Zero2Prod)
- should Jiff use something other than a "One True God" error type? Β· Issue #8 Β· BurntSushi/jiff Β· GitHub
- Guidelines for implementing Display and Error::source for library errors Β· Issue #27 Β· rust-lang/project-error-handling
- Migration plan for `Display` vs `source` guidance Β· Issue #44 Β· rust-lang/project-error-handling
- Rust error handling is perfect actually : r/rust (Added for the drawbacks mentioned in comments, ironically!)
- Error Handling in Rust with Jane Lusby :: Rustacean Station