What Is the Difference Between thiserror and anyhow?

Use thiserror for defining custom error types in libraries and anyhow for flexible error handling in applications.

The boundary problem

You're writing a Rust program. You hit an error reading a file. You want to return it. Then you realize you're also building a library that other people will use. If you return a generic error, the caller can't handle specific cases. If you define a custom error for every little thing, you drown in boilerplate. You need two different tools for two different jobs. One for the inside of your house, one for the sign on the front door.

The confusion usually starts when you see anyhow and thiserror mentioned in the same breath. Both crates deal with errors. Both make error handling easier. But they solve opposite problems. anyhow is for applications. thiserror is for libraries. Mixing them up leads to APIs that are impossible to use or codebases that are exhausting to maintain.

Type erasure versus type definition

The core difference comes down to types. anyhow erases types. thiserror defines types.

anyhow provides a single error type, anyhow::Error, that can hold any error. It works by boxing the error behind a trait object. When you use anyhow, you don't care what the specific error is. You just want to propagate the failure and print a message at the end. This is perfect for the main function or deep inside a function where multiple operations can fail in different ways.

thiserror helps you define a custom error type. It generates the implementation of std::error::Error for your struct or enum. This gives you a concrete type that callers can inspect. They can match on variants to handle specific failures. This is essential for public APIs where users need to distinguish between "file not found" and "permission denied".

Think of anyhow like a universal remote. It works with any TV. You press the power button and it works. You don't need to know if it's a Sony or a Samsung. thiserror is like the TV's manual. It tells you exactly which button does what. If you sell the TV, you give the manual. If you use the TV at home, you just use the remote.

// anyhow erases the type. Any error fits here.
use anyhow::Result;

fn load_config() -> Result<String> {
    // std::io::Error gets boxed into anyhow::Error
    let data = std::fs::read_to_string("config.json")?;
    Ok(data)
}
// thiserror defines a specific type.
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("file not found: {0}")]
    NotFound(#[from] std::io::Error),
    #[error("invalid syntax at line {0}")]
    Syntax(usize),
}

fn load_config() -> Result<String, ConfigError> {
    // Returns a concrete ConfigError
    let data = std::fs::read_to_string("config.json")?;
    Ok(data)
}

Keep anyhow inside the walls. Let thiserror guard the gate.

How they work under the hood

Understanding the mechanics helps you choose the right tool. anyhow::Error is essentially a Box<dyn std::error::Error + Send + Sync>. When you use the ? operator with anyhow, the error gets boxed and stored in the heap. This allows anyhow to hold any error type, regardless of its size or structure. The cost is a heap allocation and dynamic dispatch. For most applications, this cost is negligible. The convenience is worth it.

thiserror uses a derive macro to generate code. It creates a concrete struct or enum and implements std::error::Error, Display, and Debug for it. It also generates From implementations for source errors when you use the #[from] attribute. This allows the ? operator to convert inner errors into your custom type automatically. There is no heap allocation for the error wrapper itself. The error has a known size and structure at compile time.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    // #[from] generates impl From<std::io::Error> for AppError
    // This allows `?` to convert io::Error to AppError::Io
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    
    // Custom variant with structured data
    #[error("validation failed: field {field} is empty")]
    Validation { field: String },
}

fn process() -> Result<(), AppError> {
    // io::Error converts to AppError::Io automatically
    std::fs::read_to_string("data.txt")?;
    Ok(())
}

Convention aside: The community treats anyhow as an application crate. You'll see it in Cargo.toml under [dependencies] for binaries. You'll rarely see it in a library's public API. If you export anyhow::Error, you're signaling that you don't care about your users' ability to handle errors. Libraries should define their own error types. Applications can use anyhow to handle those types.

Context and chaining

Errors often need context. A file read failure is more helpful if you know which file failed. anyhow and thiserror handle context differently.

anyhow provides the .context() method. It wraps the error with a message. The message becomes part of the error chain. When you print the error, you see the chain of messages. This is incredibly convenient for ad-hoc error handling. You can add context at the point of failure without defining new types.

thiserror requires you to define context in the error structure. You add fields to your enum variants. This is more work upfront, but it gives you structured access to the context. Callers can extract the field name or line number programmatically. This is better for APIs where users might need to inspect the error details.

use anyhow::Result;

fn main() -> Result<()> {
    // Add context without defining a new type
    let data = std::fs::read_to_string("config.json")
        .context("Failed to read config file")?;
    
    // Add context with a lazy closure
    let value: i32 = data.parse()
        .with_context(|| format!("Failed to parse value from config"))?;
        
    Ok(())
}
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    // Context is part of the variant structure
    #[error("Failed to read config file: {0}")]
    ConfigRead(#[from] std::io::Error),
    
    #[error("Failed to parse value: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

// Callers can match on the error to get details
fn handle(err: Error) {
    match err {
        Error::ConfigRead(io_err) => {
            // Access the inner io::Error
            eprintln!("IO error: {}", io_err);
        }
        Error::Parse(parse_err) => {
            // Access the inner parse error
            eprintln!("Parse error: {}", parse_err);
        }
    }
}

anyhow also supports error chains via .chain(). This iterator yields the error and all its sources. It's useful for debugging. thiserror can nest errors, but the chain is less automatic. You usually access the source via the source() method from std::error::Error.

Don't make your users guess what went wrong. Provide context that matches the audience.

Realistic workflow

A typical Rust project uses both crates. The library defines errors with thiserror. The application handles errors with anyhow. This separation keeps the API clean and the implementation flexible.

Here's how a library might look. It defines a specific error enum. It exposes functions that return Result<T, MyError>. Users of the library can match on the error variants. They can handle specific cases.

// lib.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ParserError {
    #[error("unexpected token at byte {byte}")]
    UnexpectedToken { byte: usize },
    #[error("missing closing brace")]
    MissingBrace,
}

pub fn parse(input: &str) -> Result<Vec<Token>, ParserError> {
    // Internal logic...
    // Return specific errors
    if input.contains("{") && !input.contains("}") {
        return Err(ParserError::MissingBrace);
    }
    Ok(vec![])
}

The application uses the library. It uses anyhow to handle errors. It can wrap library errors with context. It doesn't need to define custom errors for the application logic.

// main.rs
use anyhow::Result;
use my_lib::{parse, ParserError};

fn run() -> Result<()> {
    let input = std::fs::read_to_string("input.txt")
        .context("Failed to read input file")?;
        
    let tokens = parse(&input)?;
    // ParserError converts to anyhow::Error automatically
    // because anyhow implements From for all std::error::Error types
    
    println!("Parsed {} tokens", tokens.len());
    Ok(())
}

fn main() -> Result<()> {
    run()
}

If the application needs to handle specific library errors, it can match before the ? operator. anyhow doesn't prevent matching. It just makes propagation easier.

fn run() -> Result<()> {
    let input = std::fs::read_to_string("input.txt")?;
    
    match parse(&input) {
        Ok(tokens) => println!("Parsed {} tokens", tokens.len()),
        Err(ParserError::MissingBrace) => {
            eprintln!("Please check your braces");
            // Return a custom anyhow error
            Err(anyhow::anyhow!("Syntax error: missing brace"))
        }
        Err(e) => Err(anyhow::Error::new(e).context("Parse failed")),
    }
}

Convention aside: anyhow::anyhow! is the standard way to create a new anyhow error from a message. It's concise and idiomatic. Avoid anyhow::Error::msg() unless you have a specific reason. The macro is preferred in the community.

Trust the separation. Libraries define. Applications handle.

Pitfalls and compiler errors

Leaking anyhow into a public API is the most common mistake. If your library returns anyhow::Result, callers can't match on specific errors. They get a black box. The compiler won't stop you. Your users will. They'll open issues complaining that they can't handle errors properly. Fix the API before it ships.

Using thiserror for internal error handling creates boilerplate. If you define a custom error for every function, you spend more time writing error enums than writing logic. You end up with FunctionAError, FunctionBError, and FunctionCError that all wrap the same underlying errors. Use anyhow internally to avoid this. Define custom errors only at the boundaries.

If you try to return a thiserror type where anyhow is expected, you might hit E0308 (mismatched types) if the conversion isn't set up. anyhow usually handles this automatically via From implementations for std::error::Error. If you have a custom error type that doesn't implement std::error::Error, the conversion fails. Make sure your thiserror types derive Error.

use thiserror::Error;

#[derive(Error, Debug)]
pub struct MyError(String);

fn bad() -> Result<(), anyhow::Error> {
    // This works because MyError implements std::error::Error
    Err(MyError("oops".into()))
}

fn also_bad() -> Result<(), anyhow::Error> {
    // If MyError didn't implement Error, you'd get E0277
    // trait bound `MyError: std::error::Error` is not satisfied
    Err(MyError("oops".into()))
}

Another pitfall is ignoring the error source. anyhow and thiserror both support error sources. Always chain errors properly. Don't swallow the inner error. If you create a new error from a message, attach the original error as the source. anyhow does this with .context(). thiserror does this with #[from] or by storing the source in a field.

Treat the error chain as a breadcrumb trail. If you break it, debugging becomes a guessing game.

Decision matrix

Use anyhow when you are writing an application binary and just want to get the job done. Use anyhow when you are deep inside a function and need to propagate errors from multiple sources without defining a wrapper. Use anyhow when you want to add context to errors quickly without modifying the error type. Use anyhow when the error type doesn't matter to the caller.

Use thiserror when you are writing a library and need to expose a specific error type to your users. Use thiserror when callers need to distinguish between different failure modes, like NotFound vs PermissionDenied. Use thiserror when you want to attach structured data to errors, like line numbers or field names. Use thiserror when you want to control the error message format precisely.

Reach for standard library types when the error is simple and standard. Don't reinvent the wheel. If std::io::Error or std::num::ParseIntError fits, use it. You can always wrap them later if you need more context.

Libraries define errors. Applications handle them. Stick to this rule and your code will stay clean.

Where to go next