Error Handling with anyhow and thiserror

Module 1 covered Rust's error-handling foundation — `Result`, `Option`, and the `?` operator. It works, but as programs grow, two friction points appear: defining custom error types by hand is tedious boilerplate, and propagating many different error types through `?` gets awkward. The Rust ecosystem answers with two small, near-universal crates — `thiserror` and `anyhow` — that make error handling ergonomic. Knowing which to use where is a piece of practical Rust fluency every real project needs.

Module 1 introduced Rust’s error handling (Result, Option, ?). This post covers the ecosystem’s ergonomic error-handling crates: thiserror (for defining custom error types easily) and anyhow (for easy error propagation in applications). These aren’t in the standard library but are near-universal in real Rust code, and knowing which to use when is essential practical knowledge. It builds directly on the Result/? foundation, making it ergonomic at scale.

Recap: Result, Option, and ?

Briefly recapping Module 1’s foundation, since these crates build on it:

Rust’s error foundation (Result, Option, ? — no exceptions, errors as handled data) is solid, but at scale defining custom error types is boilerplate-heavy and propagating diverse error types through ? is awkward. thiserror and anyhow address exactly these frictions. They serve different situations — libraries vs applications.

thiserror: defining error types easily

thiserror makes defining custom error types easy — it removes the boilerplate of implementing the error machinery by hand, ideal for libraries:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataError {
    #[error("file not found: {0}")]
    NotFound(String),

    #[error("invalid format")]
    InvalidFormat,

    // Automatically convert from io::Error via `?` (the #[from] attribute).
    #[error("I/O error")]
    Io(#[from] std::io::Error),
}

thiserror makes defining custom, typed error enums easy — deriving the error-trait boilerplate, declaring messages with #[error("...")], and enabling automatic conversions with #[from] (so ? propagates diverse errors into your type) — ideal for libraries that should expose specific, matchable error types. When you don’t need typed errors, anyhow is simpler.

anyhow: easy error propagation for applications

anyhow makes error propagation easy when you don’t need specific typed errors — ideal for applications:

use anyhow::{Context, Result};

// anyhow::Result<T> is Result<T, anyhow::Error> — one error type for everything.
fn load_config(path: &str) -> Result<String> {
    let contents = std::fs::read_to_string(path)
        .context("failed to read the config file")?; // add context, propagate any error
    Ok(contents)
}

anyhow makes error propagation easy for applications — providing one dynamic catch-all error type (anyhow::Error) that any error converts into (so ? propagates anything without custom types), plus .context() for helpful error context — ideal when you just need to propagate and report errors, not distinguish them. The thiserror-vs-anyhow choice is the practical crux.

Choosing and using them

Putting it together, the practical guidance for ergonomic error handling in real Rust:

Ergonomic error handling in real Rust uses thiserror (define typed error types easily — for libraries, so callers handle specific cases) and anyhow (one dynamic error type for easy propagation — for applications, just propagate and report), both building on Module 1’s Result/? foundation. The library-vs-application distinction is the practical key, and these near-universal crates are essential Rust fluency. Next, the final Module 3 post: macros.

Key takeaways

Further reading

Sources & References

Result, Option, and ?