Telex logo Telex

Rust Patterns That Matter #3: Error Handling with ? + thiserror + anyhow

Post 3 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.

Previous: #2: From / Into Conversions | Next: #4: Interior Mutability

Your application reads a config file, parses JSON, queries a database, and serves HTTP. Each layer produces its own error type. Combining them manually means writing error enums, Display impls, and From impls for every source. There's a better way - and the answer depends on whether you're writing a library or an application.

The problem

fn load_config() -> Result<Config, ???> {
    let text = std::fs::read_to_string("config.json")?; // io::Error
    let config: Config = serde_json::from_str(&text)?;   // serde_json::Error
    Ok(config)
}

Two different error types. The ? operator needs to convert both into one return type. What goes in place of ????

The manual approach

enum ConfigError {
    Io(std::io::Error),
    Parse(serde_json::Error),
}

impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "IO error: {e}"),
            ConfigError::Parse(e) => write!(f, "parse error: {e}"),
        }
    }
}

impl From<std::io::Error> for ConfigError {
    fn from(e: std::io::Error) -> Self { ConfigError::Io(e) }
}

impl From<serde_json::Error> for ConfigError {
    fn from(e: serde_json::Error) -> Self { ConfigError::Parse(e) }
}

This works, but it's 20 lines of boilerplate per error type. Add a database layer and an HTTP layer and you're writing more error plumbing than actual logic.

thiserror - for libraries

thiserror is a derive macro that generates the Display and From impls:

use thiserror::Error;

#[derive(Error, Debug)]
enum ConfigError {
    #[error("failed to read config file")]
    Io(#[from] std::io::Error),

    #[error("failed to parse config")]
    Parse(#[from] serde_json::Error),
}

The #[error("...")] attribute generates the Display impl. The #[from] attribute generates the From impl. What was 20 lines is now 8, and the intent is clear.

The key feature: callers can match on variants.

match load_config() {
    Ok(config) => { /* use config */ }
    Err(ConfigError::Io(e)) if e.kind() == ErrorKind::NotFound => {
        // file missing — use defaults
    }
    Err(e) => return Err(e.into()),
}

This is why thiserror is for libraries: downstream code can programmatically handle specific error cases. The error type is part of your API.

anyhow - for applications

In a binary (not a library), you often don't need to match on error variants. You just want to bubble errors up with context, then display them to the user. anyhow makes this trivial:

use anyhow::{Context, Result};

fn load_config() -> Result<Config> {
    let text = std::fs::read_to_string("config.json")
        .context("failed to read config file")?;
    let config = serde_json::from_str(&text)
        .context("failed to parse config")?;
    Ok(config)
}

anyhow::Result<T> is an alias for Result<T, anyhow::Error>. anyhow::Error wraps any error type (anything implementing std::error::Error). You don't define error enums at all.

.context("...") adds a human-readable message. When the error is displayed, you get a chain:

Error: failed to read config file

Caused by:
    No such file or directory (os error 2)

This is exactly what you want in an application: clear error messages with context, minimal boilerplate, and no need to define error types for every module.

The split

This is the idiomatic Rust convention:

When the line blurs

Sometimes a library's internal code doesn't need structured errors - it's private logic that will wrap errors before returning them publicly. Using anyhow internally and converting to thiserror at the public boundary is a valid approach.

Conversely, if you find yourself pattern-matching on anyhow::Error (downcasting to specific types), that's a sign you need structured errors. Switch to thiserror.

The ? operator ties it together

Everything in this post works because ? calls From::from() to convert errors (as covered in #2). thiserror generates the From impls with #[from]. anyhow provides a blanket From<E: Error> for anyhow::Error. Both patterns are built on the same conversion machinery.

When to use it

See it in practice: Building a Chat Server #1: Hello, TCP uses this pattern for handling network and parse errors with ?.