As part of learning the Rust ecosystem, I dedicated the last few days to error management. Here are my findings. Error management 101 The describes the basics of error management. The language separates between recoverable errors and unrecoverable ones. Rust book Unrecoverable errors benefit from the macro. When panics, it stops the program. Recoverable errors are much more enjoyable. panic!() Rust Rust uses the monad, which stems from Functional Programming. Opposite to exceptions in other languages, FP mandates to return a structure that may contain the requested value the error. The language models it as an with generics on each value: Either either or enum #[derive(Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub enum Result<T, E> { Ok(T), Err(E), } Because Rust manages completeness of matches, matching on enforces that you handle both branches: Result match fn_that_returns_a_result() { Ok(value) => do_something_with_value(value) Err(error) => handle_error(error) } If you omit one of the two branches, compilation fails. The above code is safe if unwieldy. But Rust offers a full-fledged API around the struct. The API implements the monad paradigm. Result Propagating results Propagating results and errors is one of the main micro-tasks in programming. Here's a naive way to approach it: #[derive(Debug)] struct Foo {} #[derive(Debug)] struct Bar { foo: Foo } #[derive(Debug)] struct MyErr {} fn main() { print!("{:?}", a(false)); } fn a(error: bool) -> Result<Bar, MyErr> { match b(error) { //1 Ok(foo) => Ok(Bar{ foo }), //2 Err(my_err) => Err(my_err) //3 } } fn b(error: bool) -> Result<Foo, MyErr> { if error { Err(MyErr {}) } else { Ok(Foo {}) } } Return a which contains a or a Result Bar MyErr If the call is successful, unwrap the value, wrap it again, and return it Foo If it isn't, unwrap the error, wrap it again, and return it The above code is a bit verbose, and because this construct is quite widespread, Rust offers the operator: ? When applied to values of the type, it propagates errors. If the value is , then it will return from the enclosing function or closure. If applied to , then it will unwrap the value to evaluate to . Result<T, E> Err(e) Err(From::from(e)) Ok(x) x -- The question mark operator We can apply it to the above function: a fn a(error: bool) -> Result<Bar, MyErr> { let foo = b(error)?; Ok(Bar{ foo }) } The trait Error Note that enforces no bounds on the right type, the "error" type. However, Rust provides an trait. Result<T, E> Error Two widespread libraries help us manage our errors more easily. Let's detail them in turn. Implement Error trait with thiserror In the above section, I described how a could implement the trait. However, doing so requires quite a load of boilerplate code. The crate provides macros to write the code for us. Here's the documentation sample: struct Error thiserror #[derive(Error, Debug)] //1 pub enum DataStoreError { #[error("data store disconnected")] //2 Disconnect(#[from] io::Error), #[error("the data for key `{0}` is not available")] //3 Redaction(String), #[error("invalid header (expected {expected:?}, found {found:?})")] //4 InvalidHeader { expected: String, found: String, } } Base macro Error Static error message Dynamic error message, using field index Dynamic error message, using field name helps you generate your errors. thiserror Propagate Result with anyhow The crate offers several features: anyhow A custom struct. I will focus on this one anyhow::Result<T> A way to attach context to a function returning an anyhow::Result<T> Additional backtrace environment variables Compatibility with thiserror A macro to create errors on the fly Result propagation has one major issue: functions signature across unrelated error types. The above snippet used a single enum, but in real-world projects, errors may come from different crates. Here's an illustration: #[derive(thiserror::Error, Debug)] pub struct ErrorX {} //1 #[derive(thiserror::Error, Debug)] pub struct ErrorY {} //1 fn a(flag: i8) -> Result<Foo, Box<dyn std::error::Error>> { //2 match flag { 1 => Err(ErrorX{}.into()), //3 2 => Err(ErrorY{}.into()), //3 _ => Ok(Foo{}) } } Two error types, each implemented with a different with struct thiserror Rust needs to know the size of the return type at compile time. Because the function can return either one or the other type, we must return a fixed-sized pointer; that's the point of the construct. For a discussion on when to use compared to other constructs, please read this . Box<dyn Error> Box StackOverflow question To wrap the struct into a , we rely on the method Box into() With , we can simplify the above code: anyhow fn a(flag: i8) -> anyhow::Result<Foo> { match flag { 1 => Err(ErrorX{}.into()), 2 => Err(ErrorY{}.into()), _ => Ok(Foo{}) } With the trait, we can improve the user experience with additional details. Context The method is evaluated lazily, while the is evaluated eagerly. with_context() context() Here's how you can use the latter: fn a(flag: i8) -> anyhow::Result<Bar> { let foo = b(flag).context(format!("Oopsie! {}", flag))?; //1 Ok(Bar{ foo }) } fn b(flag: i8) -> anyhow::Result<Foo> { match flag { 1 => Err(ErrorX{}.into()), 2 => Err(ErrorY{}.into()), _ => Ok(Foo{}) } } If the function fails, print the additional error message with the value Oopsie! flag Conclusion Rust implements error handling via the Either monad of FP and the enum. Managing such code in bare Rust requires boilerplate code. The crate can easily implement the trait for your structs while simplifies function and method signatures. Result thiserror Error anyhow To go further: Rust error handling The Error trait anyhow crate thiserror crate What is the difference between context and with_context in anyhow? Error handling across different languages A retrospective on Errors Management: where do we go from here? Originally published at on February 11th, 2024 A Java Geek