How to use anyhow crate in Rust error handling

Use the anyhow crate to simplify Rust error handling by returning a generic Result type and propagating errors with the ? operator.

The error enum fatigue

You are building a command-line tool. It reads a configuration file, parses a JSON payload, connects to a database, and writes a report. Each step can fail in a different way. The file might be missing. The JSON might have a typo. The database might be offline. In standard Rust, you define a custom error enum for each function. You implement std::error::Error for each enum. You write From conversions so the ? operator works. After three functions, you are writing more error boilerplate than actual logic.

The anyhow crate cuts through that noise. It gives you a single error type that accepts anything, tags it with context, and passes it up the call stack. You stop writing conversion glue. You start writing application logic.

How anyhow actually works

anyhow::Error is a type-erased wrapper. Think of it like a universal shipping box. You do not need to know whether the package inside is a book, a circuit board, or a wrench. You just seal the box, slap a label on it, and hand it to the next person in the chain. Under the hood, anyhow boxes the original error type and stores it alongside a string of context and a stack trace. The compiler sees only one type: anyhow::Error.

This erasure is what makes the ? operator work without manual conversion code. The anyhow crate implements the From<E> trait for every type E that implements std::error::Error. When you write ? on a std::io::Error, the compiler automatically calls From::from. The error gets boxed, wrapped in anyhow::Error, and returned. The chain continues until it hits a function that explicitly handles it or reaches main.

Type erasure trades compile-time specificity for runtime flexibility. You lose the ability to pattern match on the exact error variant. You gain the ability to propagate failures through dozens of layers without rewriting a single conversion function.

A minimal working example

Add the dependency to your manifest file. The crate follows semantic versioning, so pinning the major version is standard practice.

[dependencies]
anyhow = "1"

Import the type alias and the macro. The alias replaces std::result::Result in your function signatures. The macro builds errors from format strings.

use anyhow::{anyhow, Result};

/// Reads a configuration file and returns its contents.
fn read_config() -> Result<String> {
    // The ? operator automatically converts std::io::Error
    // into anyhow::Error via the From trait implementation.
    let content = std::fs::read_to_string("config.txt")?;
    
    // Validate the payload before returning it.
    // The anyhow! macro formats the string and wraps it
    // in an anyhow::Error without requiring a custom enum.
    if content.is_empty() {
        return Err(anyhow!("Config file is empty"));
    }
    
    // Return the successfully loaded configuration.
    Ok(content)
}

The compiler resolves the ? operator by looking for a From implementation. anyhow provides exactly that. You get automatic conversion with zero boilerplate.

Following the chain at runtime

When read_config fails, the error travels upward. Each layer can attach context before passing it along. Context turns a cryptic system message into a useful diagnostic.

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

/// Loads and validates the application configuration.
fn load_app_config() -> Result<()> {
    // Attach a static context string to the error.
    // This runs only when the error path is taken.
    let config = std::fs::read_to_string("config.txt")
        .context("Failed to read configuration file")?;
    
    // Attach a dynamic context using a closure.
    // The closure receives the error and returns a format string.
    let data = parse_json(&config)
        .with_context(|e| format!("JSON parse failed: {}", e))?;
    
    Ok(())
}

/// Parses a JSON string into a placeholder structure.
fn parse_json(input: &str) -> Result<()> {
    Err(anyhow!("Invalid JSON structure"))
}

At runtime, anyhow captures a stack trace when the error is created. The context strings are stored in a linked list inside the error wrapper. When you print the error with {:#}, anyhow walks the chain and prints each context line alongside the original cause. The stack trace points to the exact line where the failure originated.

Convention note: use .context() for static strings and .with_context() for dynamic formatting. The closure in .with_context() is lazy. It only runs if the operation actually fails. This saves CPU cycles on the success path.

Trust the context chain. A well-tagged error saves hours of debugging.

Real-world error chaining

Application code rarely stops at one function. You usually have a command handler, a service layer, and an I/O layer. anyhow shines when you need to bubble failures through all three without losing track of where they happened.

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

/// Executes the main application workflow.
fn run() -> Result<()> {
    // Delegate to the service layer.
    // Errors bubble up automatically.
    let data = fetch_data()?;
    
    // Process the data and attach application-level context.
    process_data(&data)
        .context("Data processing pipeline failed")?;
    
    Ok(())
}

/// Fetches raw data from an external source.
fn fetch_data() -> Result<Vec<u8>> {
    // Simulate a network or file read operation.
    // The ? operator converts the underlying error type.
    std::fs::read("data.bin")
        .context("Could not fetch raw data")
}

/// Processes the fetched data.
fn process_data(_data: &[u8]) -> Result<()> {
    // Simulate a validation step that might fail.
    Err(anyhow!("Data checksum mismatch"))
}

The ? operator does the heavy lifting. It checks for Ok. If it finds Err, it calls From::from to convert the error into anyhow::Error, then returns early. The context methods wrap that conversion. You get a clean call stack and a readable error message.

Do not fight the type system here. Let anyhow handle the propagation.

Where the type system fights back

Type erasure has a cost. You cannot pattern match on anyhow::Error. If you try to match on a specific error variant, the compiler rejects you with E0308 (mismatched types) or E0277 (trait bound not satisfied). The compiler needs a concrete type to match against. anyhow::Error is a black box.

use anyhow::{anyhow, Result};

/// Attempts to match on an anyhow error directly.
fn bad_match() -> Result<()> {
    let err: anyhow::Error = anyhow!("Something broke");
    
    // This fails to compile. anyhow::Error does not expose
    // its inner type, so pattern matching is impossible.
    match err {
        // E0308: mismatched types. The compiler expects
        // a concrete variant, not a type-erased wrapper.
        MyError::Custom => println!("Caught it"),
        _ => println!("Other"),
    }
    
    Ok(())
}

You can downcast using err.downcast_ref::<std::io::Error>(), but it requires knowing the exact original type. It defeats the purpose of type erasure. The community convention is simple: do not match on anyhow errors. Handle them at the boundary. Print them, log them, or convert them to an exit code.

Convention note: keep anyhow out of library crates. Libraries should expose specific error types so downstream users can match on them. Applications should use anyhow to absorb those specific errors and handle them uniformly. Put the type erasure at the top of the stack, not in the middle.

Treat the library boundary as a firewall. Keep anyhow on the application side.

Picking the right error strategy

Error handling in Rust depends on where your code lives and what it needs to do. Choose the tool that matches the boundary.

Use anyhow when you are writing application code, CLI tools, or scripts where errors only need to be printed or logged. Use anyhow when you want to chain context strings across multiple layers without defining custom enums. Use anyhow when you need automatic ? propagation across different error types from third-party crates.

Use thiserror when you are building a library crate and need to expose a public error type that downstream users can match on. Use thiserror when you want derive macros to generate Display and Error implementations with minimal boilerplate. Use thiserror when your error variants need to carry custom data that consumers must inspect.

Use custom error enums when you need fine-grained control over error conversion, when you are implementing a domain-specific protocol, or when you need to satisfy trait bounds that anyhow cannot provide. Use std::io::Error when you are writing low-level I/O wrappers and want to stay within the standard library. Use std::result::Result<T, E> with explicit E types when the error type is known and stable across the entire module.

Pick the boundary that matches your crate type. Applications get anyhow. Libraries get thiserror.

Where to go next