C.W.K.
Stream
Lesson 05 of 05 · published

thiserror & anyhow

~11 min · errors, thiserror, anyhow, ecosystem

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

Hand-writing Display, Error, and From for every error variant is real boilerplate. Two crates own this space, and which you choose says something about what you're building.

thiserror — for libraries

thiserror generates the boilerplate for a concrete error enum. You write the variants and annotate them with #[error("...")]; the macro derives Display, Error, and (with #[from]) the From conversions. The result is a precise, matchable error type — exactly what a library should expose so its callers can react to specific failures.

anyhow — for applications

anyhow gives you one catch-all type, anyhow::Error, that any error converts into. You stop naming error types and just ? everything, adding context with .context("while loading config"). It's perfect for application code — a CLI, a binary — where you don't need callers to match on error kinds; you just want a good message and a backtrace.

Libraries expose concrete errors (thiserror); applications consume with one type (anyhow). Rule of thumb: if other code will match on your errors, give them a real enum via thiserror. If you're at the top of the stack and just want failures to propagate with context, use anyhow. Many real projects use both — thiserror in the library crates, anyhow in the binary.

You've earned these

Notice what these crates are: macro-generated trait implementations. thiserror derives From and Display for you; anyhow leans on Box<dyn Error> and the ? conversion you already understand. They're not magic — they're the patterns from this track, automated. That's the Rust ecosystem in miniature: a small crate that removes boilerplate around a language feature you already know.

Code

thiserror derives the boilerplate·rust
// Cargo.toml:  thiserror = "2"  (libraries)   anyhow = "1"  (apps)
use thiserror::Error;

#[derive(Error, Debug)]
enum DataError {
    #[error("not found")]
    NotFound,
    #[error("bad input: {0}")]
    BadInput(String),
    #[error("io failure")]
    Io(#[from] std::io::Error), // #[from] auto-generates From + ? support
}

fn load() -> Result<String, DataError> {
    Err(DataError::BadInput("empty".into()))
}

fn main() {
    if let Err(e) = load() {
        println!("{e}"); // uses the #[error(...)] Display string
    }
}

External links

Exercise

Take the custom ConfigError enum from the previous lesson and rewrite it with thiserror: derive Error, annotate each variant with #[error("...")], and add a variant that wraps std::io::Error with #[from]. Confirm ? now converts an io error into your type automatically. How many lines did the macro save?
Hint
The #[error(...)] attributes replace your hand-written Display, and #[from] replaces the hand-written From impl. You typically delete 15-20 lines of boilerplate per error type — that's why thiserror is near-universal in library crates.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Ttoriwarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.