How to use anyhow crate

Add anyhow to Cargo.toml and use Result<T> with the ? operator to simplify error handling in Rust.

The error boilerplate trap

You are building a command-line tool that reads a configuration file, fetches data from an API, and writes a report. In Python, you would wrap the logic in a try/except block and let exceptions bubble up. In Rust, you hit a wall. Every function needs a return type. Every return type needs an error type.

You start by defining enum ConfigError. Then you need enum ApiError. Your main function calls both, so you define enum AppError with variants for each. You implement std::error::Error for AppError. You implement Display. You write From conversions so the ? operator works. You spend forty lines of code just to glue error types together. Your actual business logic is three lines. The error handling boilerplate is eating your progress.

Stop writing glue code. Let anyhow handle the plumbing.

A universal error type

anyhow provides a single error type, anyhow::Error, that can hold any error. It implements the From trait for every type that implements std::error::Error. This means the ? operator automatically converts any error into anyhow::Error without explicit conversion code.

Think of anyhow::Error like a universal adapter for electrical outlets. You do not care if the device uses a two-prong plug, a three-prong plug, or a foreign standard. You plug it into the adapter, and it works. In Rust terms, anyhow::Error wraps the concrete error, preserves the original type information, and adds a chain of context messages. It also captures a backtrace automatically when enabled.

The error type becomes a universal sink. Everything fits.

Minimal setup

Add anyhow to your dependencies. The crate provides a Result type alias that saves typing.

[dependencies]
anyhow = "1"

In your code, import anyhow::Result and use it as the return type. The ? operator propagates errors automatically.

use anyhow::{anyhow, Result};

fn main() -> Result<()> {
    // Result<()> is sugar for Result<(), anyhow::Error>.
    // Returning an error from main prints a report and exits.
    let value = do_work()?;
    println!("Got: {}", value);
    Ok(())
}

fn do_work() -> Result<String> {
    // Simulate a failure.
    if true {
        // anyhow! creates an error with a message and a backtrace.
        return Err(anyhow!("Something broke in do_work"));
    }
    Ok("Success".to_string())
}

The ? operator does the heavy lifting. You just write the logic.

How anyhow wraps the world

When you call anyhow!("message"), the macro creates an error containing the formatted message and a lazy backtrace. When you use ? on a function returning Result<T, E>, Rust calls From::from to convert E into anyhow::Error. Since anyhow implements From<E> for all E: std::error::Error, this conversion happens silently.

The anyhow::Error type is a smart pointer to a heap-allocated error chain. It stores the source error and a list of context messages. When you print the error, anyhow walks the chain and displays each message, followed by the source error and the backtrace.

This design beats Box<dyn std::error::Error> in three ways. First, anyhow captures backtraces automatically when RUST_BACKTRACE=1 is set. Second, anyhow provides the Context trait for adding messages to the chain. Third, anyhow avoids the trait object overhead in some hot paths by using a vtable-free representation for simple errors.

The error chain turns a cryptic failure into a debuggable report.

Chaining context

Propagating errors with ? works, but it loses information about where the error happened. If fs::read_to_string fails, you get an I/O error. You do not know which file caused the problem. Use the Context trait to add a message to the error chain.

use anyhow::{Context, Result};
use std::fs;

fn run() -> Result<()> {
    // Read the config file.
    let content = fs::read_to_string("config.json")
        // context adds a message to the error chain.
        // If the read fails, the error becomes:
        // "Failed to read config: No such file or directory"
        .context("Failed to read config")?;

    // Parse the JSON.
    let config: Config = serde_json::from_str(&content)
        .context("Config file is malformed")?;

    process(config);
    Ok(())
}

#[derive(serde::Deserialize)]
struct Config {
    name: String,
}

fn process(_config: Config) {
    // Logic here.
}

The Context trait is implemented for anyhow::Result<T>, not for anyhow::Error. This is intentional. The trait returns a Result, so it can preserve the success type T while wrapping the error. You cannot add context to an error value directly; you must add it to the Result carrying the error.

There is a performance nuance here. The .context("msg") method allocates the context string immediately, even if the operation succeeds. If the context message is expensive to compute, use .with_context(|e| format!("msg: {}", e)). The closure runs only when an error occurs.

Context is king. An error without context is a mystery.

Backtraces and debugging

anyhow captures backtraces lazily. When you create an error, the backtrace is empty. When you print the error or access the backtrace, anyhow captures the stack. This avoids the performance cost of capturing backtraces for every error, while still providing them when you need them.

Set the RUST_BACKTRACE environment variable to 1 to see backtraces in your output.

RUST_BACKTRACE=1 cargo run

The output includes the full stack trace for the error, pointing directly to the line where it occurred. This is invaluable for debugging complex applications.

Enable backtraces in development. They save hours.

The library boundary rule

The Rust community has a strict convention about anyhow. Use anyhow in binary crates and application code. Never export anyhow::Error from a library crate.

Library crates must expose their error types so users can match on them. If your library returns anyhow::Error, users cannot distinguish between different error conditions. They cannot recover from specific failures. anyhow::Error is opaque; it hides the concrete type.

If you are writing a library, define a custom error enum and implement std::error::Error. Use the thiserror crate to generate the boilerplate. thiserror lets you define error variants and derive Display and Error implementations with minimal code.

Libraries define errors. Applications consume them. Keep that boundary sharp.

Pitfalls

You cannot pattern match on anyhow::Error. The error type is a black box. If you try to match on it, the compiler rejects the code with E0277 (trait bound not satisfied) or a similar error about not being able to match on a boxed trait object.

use anyhow::Result;

fn handle_error(err: anyhow::Error) {
    // This does not compile.
    // match err {
    //     std::io::ErrorKind::NotFound => println!("Not found"),
    //     _ => println!("Other error"),
    // }
}

To recover from a specific error type inside anyhow, use downcast_ref. This checks if the error chain contains a specific type and returns a reference to it.

use anyhow::Result;
use std::io;

fn handle_error(err: anyhow::Error) {
    // Check if the error is an I/O error.
    if let Some(io_err) = err.downcast_ref::<io::Error>() {
        if io_err.kind() == io::ErrorKind::NotFound {
            println!("File not found");
            return;
        }
    }
    // Handle other errors.
    eprintln!("Error: {}", err);
}

Downcasting works on the source error and any context messages in the chain. It is the only way to inspect the contents of an anyhow::Error.

You cannot match on a black box. Handle errors where you know the type.

Decision matrix

Use anyhow when you are writing an application binary and want to stop writing boilerplate error enums. Use anyhow when you need to chain errors with context messages using .context(). Use anyhow when you want automatic backtrace capture for debugging.

Use thiserror when you are writing a library and need to expose specific error variants to your users. Use thiserror when users of your crate need to match on error types to recover from failures.

Use std::io::Result when your function only deals with file system or network I/O and you do not need custom error logic. Use std::io::Result when you are writing low-level code that interacts with the OS and want to avoid heap allocations.

Reach for custom enum errors when you need to recover from specific error conditions inside the same module. Reach for custom errors when the error type carries data that the caller needs to inspect.

Pick the tool that matches your crate type. The compiler will thank you.

Where to go next