Error Handling with thiserror & anyhow

Reviewed & published by Brayan K

Rust's Result and ? operator are the foundation, but two crates make real-world error handling effortless: thiserror for precise library errors, and anyhow for ergonomic application-level reporting.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll derive typed error enums with thiserror, propagate and enrich errors with anyhow's context and macros, and learn exactly when to reach for each.

What You'll Learn in This Lesson

1️⃣ Typed Library Errors with thiserror

A library should expose a precise error type its callers can match on. #[derive(Error)] implements std::error::Error, #[error("...")] supplies the Display message, and #[from] lets ? auto-convert a source error.

// thiserror: define a precise error TYPE for a library.
// Callers can match on each variant.
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    // #[error("...")] is the Display message; {0} interpolates the field.
    #[error("config file not found: {0}")]
    NotFound(String),

    // #[from] generates From<std::num::ParseIntError>, so ? auto-converts.
    #[error("invalid number in config")]
    BadNumber(#[from] std::num::ParseIntError),

    #[error("missing required key: {key}")]
    MissingKey { key: String },
}

fn parse_port(raw: &str) -> Result<u16, ConfigError> {
    // The ? converts ParseIntError into ConfigError::BadNumber via #[from].
    let port: u16 = raw.parse()?;
    Ok(port)
}

fn main() {
    match parse_port("oops") {
        Ok(p) => println!("port = {p}"),
        Err(e) => println!("error: {e}"),
    }
}

Because BadNumber carries #[from], the ? in parse_port turned a ParseIntError into our own typed variant with no manual conversion.

2️⃣ Application Errors with anyhow

In application code you usually just want to propagate and report. anyhow::Result<T> is Result<T, anyhow::Error>, so ? accepts any error, and .context attaches a helpful note as it bubbles up.

// anyhow: one easy error type for APPLICATIONS.
// Great when you just want to propagate and report, not match.
use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<String> {
    // ? works with ANY error type here; .context adds a helpful message.
    let text = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config at {path}"))?;
    Ok(text)
}

fn main() -> Result<()> {
    let config = load_config("/no/such/file.toml")?;
    println!("loaded {} bytes", config.len());
    Ok(())
}

Returning anyhow::Result<()> from main printed the full cause chain — your context plus the underlying OS error.

3️⃣ Early Exits with bail! and ensure!

anyhow's macros make guard clauses concise. bail!("msg") returns an error immediately, and ensure!(cond, "msg") bails only when the condition is false — like assert!, but returning instead of panicking.

// bail! returns early with an error; ensure! checks a condition.
use anyhow::{bail, ensure, Result};

fn withdraw(balance: i64, amount: i64) -> Result<i64> {
    // ensure!(cond, msg): if cond is false, return Err with the message.
    ensure!(amount > 0, "amount must be positive, got {amount}");

    if amount > balance {
        // bail! is shorthand for: return Err(anyhow!(...));
        bail!("insufficient funds: have {balance}, need {amount}");
    }

    Ok(balance - amount)
}

fn main() -> Result<()> {
    println!("new balance = {}", withdraw(100, 30)?);
    println!("new balance = {}", withdraw(100, 250)?); // triggers bail!
    Ok(())
}

These keep validation readable: the happy path stays at the top level while invalid inputs exit early with a clear message.

Mini-Challenge: A Typed MathError

Define a small thiserror enum and return it from a fallible function.

// 🎯 MINI-CHALLENGE: a typed library error with thiserror.
//
// 1. Define  #[derive(Error, Debug)] enum MathError  with one variant
//    DivideByZero, giving it #[error("cannot divide by zero")].
// 2. Write  fn divide(a: i32, b: i32) -> Result<i32, MathError>  that
//    returns Err(MathError::DivideByZero) when b == 0, else Ok(a / b).
// 3. In main, call divide(10, 0) and print the error.
//
// Expected output:  error: cannot divide by zero

// your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Errors

GoalRust Syntax
Typed error enum#[derive(Error, Debug)] enum E {}
Display message#[error("not found: {0}")]
Auto convert with ?V(#[from] OtherError)
App resultfn f() -> anyhow::Result<T>
Add context.context("while loading")?
Early errorbail!("bad input")

🎉 Lesson Complete!

Practice quiz

Which crate is designed for defining error types in libraries?

  • thiserror
  • tokio
  • clap
  • anyhow

Answer: thiserror. thiserror helps libraries declare precise, typed error enums that callers can match on.

Which crate is aimed at applications that just need easy error propagation?

  • serde
  • rayon
  • anyhow
  • thiserror

Answer: anyhow. anyhow gives applications a single, ergonomic error type for reporting, not for callers to match on.

What does thiserror's #[derive(Error)] do?

  • Logs errors automatically
  • Generates a std::error::Error implementation for your type
  • Catches panics
  • Spawns a retry task

Answer: Generates a std::error::Error implementation for your type. #[derive(Error)] implements std::error::Error (and helpers) for your custom error enum.

What does the #[error("...")] attribute provide?

  • A backtrace flag
  • A retry count
  • A log level
  • The Display message for that variant

Answer: The Display message for that variant. #[error("...")] supplies the Display text for an error variant, with field interpolation like {0} or {path}.

What does #[from] on an error variant's field generate?

  • A From conversion so ? can auto-convert that source error
  • A Default impl
  • A Clone impl
  • A Hash impl

Answer: A From conversion so ? can auto-convert that source error. #[from] derives From for the wrapped error so the ? operator converts it into your error type automatically.

What is the type alias anyhow::Result<T> equal to?

  • Result<T, Box<str>>
  • Result<T, anyhow::Error>
  • Option<T>
  • Result<T, String>

Answer: Result<T, anyhow::Error>. anyhow::Result<T> is shorthand for Result<T, anyhow::Error>, a dynamic error that wraps almost anything.

What does the .context("...") method from anyhow add?

  • A timeout
  • A mutex guard
  • A new thread
  • Extra descriptive context to an error as it propagates

Answer: Extra descriptive context to an error as it propagates. .context(...) attaches a human-readable message, building a helpful chain as the error bubbles up.

What does anyhow's bail!("msg") macro do?

  • Retries the operation
  • Logs and continues
  • Returns early with an error built from the message
  • Panics the thread

Answer: Returns early with an error built from the message. bail! is shorthand for returning Err(anyhow!(...)) early from the current function.

Compared to Box<dyn Error>, what does anyhow::Error add?

  • Compile-time variant checking
  • Captured backtraces and easy .context, plus single-word ergonomics
  • Zero-cost typed matching
  • Automatic logging to a file

Answer: Captured backtraces and easy .context, plus single-word ergonomics. anyhow::Error wraps any error like Box<dyn Error> but adds context chaining and backtrace capture.

What is the recommended split between the two crates?

  • thiserror for library error types, anyhow for application-level handling
  • Never use either
  • thiserror everywhere
  • anyhow everywhere

Answer: thiserror for library error types, anyhow for application-level handling. Libraries expose typed errors with thiserror; applications consume and report them ergonomically with anyhow.

Continue this course

Frequently asked questions

When should I use thiserror versus anyhow?

The community rule of thumb is: thiserror for libraries, anyhow for applications. A library should expose a precise, typed error so its callers can inspect and match on specific failures and decide how to react — thiserror makes defining that enum painless. An application, on the other hand, usually just wants to propagate errors up to main and print a useful report; it rarely needs to programmatically distinguish every failure, so anyhow's single dynamic error type and ergonomic helpers are ideal. The two compose well: a binary can use anyhow at the top level while still depending on libraries whose functions return their own thiserror types, because anyhow can absorb any of them.

What do #[derive(Error)], #[error("...")], and #[from] actually generate?

On an enum, thiserror's #[derive(Error)] generates a complete std::error::Error implementation, including the source() method that links an error to its underlying cause. The #[error("...")] attribute on each variant provides that variant's Display message, and it can interpolate fields: {0} for tuple fields or {name} for named fields. Putting #[from] on a single field generates a From conversion from that field's type into your error enum, which is what lets the ? operator automatically wrap a lower-level error (like a ParseIntError) into the matching variant of your error type. Together these remove almost all of the boilerplate of hand-writing error enums.

How do anyhow's Result, Context, bail!, and ensure! work together?

anyhow::Result<T> is just an alias for Result<T, anyhow::Error>, where anyhow::Error can hold essentially any error. Because of that, the ? operator works on almost anything inside an anyhow function. The Context trait adds .context("msg") and .with_context(|| ...) so you can attach a descriptive note as an error travels upward, producing a readable causal chain. The bail! macro is shorthand for returning early with a freshly built error (return Err(anyhow!(...))), and ensure!(condition, message) bails only when the condition is false, much like assert! but returning an error instead of panicking. Returning anyhow::Result<()> from main even prints the whole context chain for you.

How do these compare to plain Box<dyn Error>?

Box<dyn std::error::Error> is the standard-library way to hold any error behind a trait object, and it requires no extra crates. anyhow::Error fills a similar role but adds real ergonomics: it captures a backtrace (when enabled), offers the .context chaining API for richer messages, prints a nicely formatted cause chain, and is a single concrete type that is convenient to thread through your code. So anyhow is essentially a more capable, friendlier Box<dyn Error> for application code. thiserror sits at the opposite end of the spectrum: instead of erasing the type, it gives you a concrete, exhaustively matchable error enum, which is exactly what library consumers want.