How to Use the anyhow Crate for Application Errors

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

When error types collide

You are building a command-line tool that reads a config file, parses JSON, and fetches data from an API. The first function fails if the file is missing. The second fails if the JSON is malformed. The third fails if the network drops. You write the first function and return Result<String, std::io::Error>. The second returns Result<Data, serde_json::Error>. The third returns Result<Response, reqwest::Error>.

Now you need a function that calls all three. The compiler demands a single error type. You try Result<(), std::io::Error>, but the JSON error does not fit. You try Box<dyn std::error::Error>, and suddenly every function signature looks like a legal contract written in a foreign language. You spend more time wrestling with error types than solving the actual problem.

The universal adapter

anyhow solves this by providing a single error type that can hold any other error. Think of it like a universal power adapter. You have a laptop charger, a phone charger, and a camera charger, each with a different plug shape. Instead of carrying three adapters, you carry one universal adapter that accepts any plug and outputs standard power. anyhow::Error is that adapter. It wraps whatever error you throw at it and presents a uniform interface.

The crate implements From for nearly every standard library error and most common third-party errors. This means the ? operator works without extra code. You write file.read()?, and if read fails with an io::Error, anyhow catches it, wraps it, and returns an anyhow::Error. The caller sees one consistent type. The error details are preserved inside the wrapper.

Use anyhow in application crates where you control the top-level error handling. It is designed for binaries, CLI tools, and scripts. It is not designed for libraries. Libraries need to expose specific error types so callers can recover programmatically. anyhow hides those types behind a dynamic wrapper. Keep anyhow in the app. Keep thiserror in the library.

Minimal example

Add anyhow = "1" to your Cargo.toml. Import anyhow::Result and the anyhow! macro. Define functions to return Result types that automatically convert other errors.

use anyhow::{anyhow, Result};

/// Reads a config file and returns its contents.
/// Returns an anyhow::Result which can hold any error type.
fn read_config() -> Result<String> {
    // std::fs::read_to_string returns Result<String, std::io::Error>.
    // The ? operator automatically converts io::Error to anyhow::Error.
    let content = std::fs::read_to_string("config.txt")?;

    // Custom errors need the anyhow! macro to convert to anyhow::Error.
    // The macro supports formatting arguments like println!.
    if content.is_empty() {
        return Err(anyhow!("Config file is empty"));
    }

    Ok(content)
}

fn main() -> Result<()> {
    // main can return Result<()> to let errors print to stderr automatically.
    // The runtime handles the error display, so you don't need a match block.
    let config = read_config()?;
    println!("Config: {}", config);
    Ok(())
}

The community convention is to import anyhow::Result and use it directly. Writing Result<T, anyhow::Error> works but is verbose. Everyone reads Result<T> and knows it means "anyhow result" in application code. Also, use the anyhow! macro for custom messages. It is shorter and supports formatting arguments. Avoid anyhow::Error::msg unless you have a specific reason.

Let ? do the heavy lifting. Write anyhow! only when you have a message to add.

How the chain works

Under the hood, anyhow::Error is a smart pointer. It points to a heap allocation that holds the error chain. The chain is a linked list of errors. Each node has a message and a source error. When you print the error, anyhow walks the chain and formats it.

When you call read_config(), the function returns Result<String, anyhow::Error>. Inside, std::fs::read_to_string might fail. The ? operator checks the result. If it is Ok, it unwraps the string. If it is Err, the operator calls From::from on the error. anyhow implements From<std::io::Error>, so the IO error gets wrapped inside an anyhow::Error and returned immediately. If the file is empty, anyhow!("Config file is empty") creates a new anyhow::Error with that message.

The error type is opaque to the caller. The caller does not know if the error came from the filesystem or the empty check. It just knows something went wrong. This opacity is a feature. It prevents callers from depending on specific error types. It forces you to handle errors at the boundary where you have enough context to decide what to do.

anyhow::Error requires the inner error to implement Send and Sync. This means you can send anyhow::Error across threads. This is crucial for async code. If an error does not implement Send, anyhow will not accept it. You will get a compiler error about trait bounds, usually E0277 (trait bound not satisfied). Most standard errors implement Send + Sync, so this rarely bites you.

The error chain preserves the root cause. When you print an anyhow::Error, you see the full chain. The message appears first, followed by the source errors. This makes debugging easier. You get the context you added plus the original error details.

Context turns a cryptic "No such file" into a helpful "Failed to load config: No such file".

Realistic usage with context

In real code, you often need to add descriptive messages to errors. anyhow provides the Context trait for this. The method .context("message") wraps the error and attaches the message. This creates a chain. When printed, you see "Failed to read config.json: No such file or directory".

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

/// Represents a user configuration.
struct Config {
    name: String,
    timeout: u64,
}

/// Loads and parses the configuration.
/// Uses Context to add descriptive messages to errors.
fn load_config() -> Result<Config> {
    // Read the file. Context adds "Failed to read config" to the error chain.
    // The original io::Error is preserved as the source.
    let content = std::fs::read_to_string("config.json")
        .context("Failed to read config.json")?;

    // Parse JSON. Context adds "Failed to parse JSON" to the error chain.
    // serde_json::Error is wrapped and linked to the context message.
    let config: Config = serde_json::from_str(&content)
        .context("Failed to parse config.json as JSON")?;

    // Validate fields. Custom error with context.
    // Use anyhow! for the message, then chain it with context.
    if config.timeout == 0 {
        return Err(anyhow!("Timeout cannot be zero"))
            .context("Invalid configuration values");
    }

    Ok(config)
}

fn main() -> Result<()> {
    // Match on the result to handle success and failure explicitly.
    // This is useful when you want custom error handling logic.
    match load_config() {
        Ok(config) => println!("Loaded config for {}", config.name),
        Err(e) => {
            // anyhow prints the full error chain with context.
            // eprintln! sends the error to stderr, which is standard practice.
            eprintln!("Error: {}", e);

            // You can iterate the chain to inspect each link.
            // skip(1) skips the top-level error to show causes.
            for cause in e.chain().skip(1) {
                eprintln!("  Caused by: {}", cause);
            }
        }
    }
    Ok(())
}

The community prefers .context() over creating a new error with anyhow! when you want to preserve the original error. Use .context() to wrap existing errors. Use anyhow! to create new errors from scratch. Use .with_context(|| "message") if the message is expensive to compute. The closure is only called if an error occurs.

anyhow works seamlessly with async functions. You can return Result<T> from async fn. The error type is the same. The ? operator works inside async blocks. Just ensure the errors you wrap implement Send + Sync. Most HTTP client errors and IO errors do.

use anyhow::Result;

/// Fetches data asynchronously.
/// anyhow::Result works seamlessly with async functions.
async fn fetch_data() -> Result<String> {
    // reqwest::Error implements Send + Sync, so it works with anyhow.
    // The ? operator propagates the error as anyhow::Error.
    let response = reqwest::get("https://api.example.com/data")
        .await
        .context("Failed to fetch data")?;

    // Read the response body.
    let text = response.text().await
        .context("Failed to read response body")?;

    Ok(text)
}

Async code does not change the rules. Use anyhow the same way. Add context. Propagate with ?. Handle at the top level.

Pitfalls and boundaries

Never use anyhow in a library crate. Libraries should define their own error types or use thiserror. anyhow hides the specific error type behind a dynamic wrapper. If your library returns anyhow::Error, the caller cannot check if the error is a specific type. They cannot recover from it programmatically. They only get a string. This breaks the contract between library and user.

You cannot pattern match on anyhow::Error to check for specific error types. The error is opaque. If you write match err { MyError::Timeout => ... }, the compiler rejects it with E0004 (non-exhaustive patterns) or type mismatch because anyhow::Error does not expose its inner type. You must use downcast_ref if you absolutely need to inspect the error, but this defeats the purpose of anyhow. Downcasting is a last resort. It requires knowing the exact type and breaks if the library changes its error implementation.

If you import anyhow::Result and also use std::result::Result in the same scope, the compiler gets confused. You will see E0428 (a type named Result is already in scope). Always use use anyhow::Result; in application code and avoid importing std::result::Result explicitly. Rely on the prelude for standard results if needed, or use fully qualified paths.

anyhow adds a small runtime cost. The error chain is allocated on the heap. The From implementations add a layer of indirection. For most applications, this cost is negligible. Error handling is rarely on the hot path. If you are writing performance-critical code that generates millions of errors per second, measure first. You might need a custom error type with zero-cost allocation. For 99% of code, anyhow is fast enough.

Libraries define errors. Applications handle them. Keep anyhow in the app.

Choosing the right tool

Use anyhow in application crates where you control the top-level error handling and want concise code. Use anyhow when you are writing a binary, a CLI tool, or a script and need to propagate errors from many different sources without defining a custom error enum. Use thiserror in library crates where you need to expose specific error types to callers so they can match and recover. Use std::io::Error or simple Result<T, E> when the error domain is narrow and well-defined, like a single module that only deals with file operations. Use Box<dyn std::error::Error> only when you cannot depend on external crates and need a generic error type, though anyhow is preferred when dependencies are allowed.

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

Where to go next