5 min read #rust #Error Handling
On this page

Error Trait and Ecosystem

std::error::Error is merely a combination of Display + Debug + source()—it is too thin. thiserror uses derive macros to generate structured error types for libraries (allowing callers to match), while anyhow provides a universal error wrapper for applications that "doesn't care about the error type, just propagates it upward." Libraries use thiserror, applications use anyhow—this is not dogma, but a division of labor refined through practice.

std::error::Error trait

The standard library's Error trait is simple—Display + Debug + source():

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError { msg: String, source: Option<Box<dyn Error + 'static>> }

impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}", self.msg)
}}

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_deref() }
}

The source() method returns the underlying cause, forming an error chain. anyhow and eyre leverage this chain to automatically generate backtraces.

thiserror: derive macro for library crates

Used for libraries—providing concrete, matchable error types to callers:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data not found: {0}")]
    NotFound(String),
    #[error("IO error")]
    Io(#[from] std::io::Error),      // #[from] = auto impl From + auto source
    #[error("invalid config key={key}")]
    InvalidConfig { key: String, detail: String },
}

#[from] automatically generates From<io::Error> for DataStoreError, allowing the ? operator to perform automatic conversion. Callers can use match to distinguish error types.

anyhow: generic error container for application code

Used for applications/binaries—collecting error chains without caring about specific types:

use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<Config> {
    let file = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path))?;
    let config: Config = toml::from_str(&file)
        .context("invalid TOML format")?;
    Ok(config)
}

anyhow::Error can store any Error + Send + Sync + 'static. context() adds human-readable comments to the error chain—this is the most common pattern in anyhow.

When to use thiserror vs anyhow

A simple rule: Libraries use thiserror (giving callers choice), applications use anyhow (collecting error context). Libraries should not force callers to use a specific error reporting library—throw concrete, matchable types. Applications do not need to distinguish error types—they need all errors to be caught, logged, and have sufficient context to reproduce bugs.

References

  • thiserror: docs.rs/thiserror
  • anyhow: docs.rs/anyhow

Keywords: Error trait, thiserror, anyhow, context, source, error chain, #[from]