anyhow (Rust): Error Context & vs thiserror Guide

Max WellsMax WellsFounder of Rustify

TL;DR: anyhow is the simplest way to do application-level error propagation in Rust in 2026. Use it in binaries, CLIs, and server glue code when you want ?, context, and readable top-level errors without designing a custom error enum for every layer. Do not use anyhow as your public library error type when callers need to match on structured variants.


What Is anyhow?

anyhow erases concrete error types into a single anyhow::Error, and it is the default ergonomic choice for application-layer Rust error handling in 2026.

# Cargo.toml
[dependencies]
anyhow = "1"
use anyhow::Result;
 
fn read_config(path: &str) -> Result<String> {
    let content = std::fs::read_to_string(path)?;  // std::io::Error → anyhow::Error
    let trimmed = content.trim().to_string();
    Ok(trimmed)
}
 
fn main() -> Result<()> {
    let config = read_config("config.toml")?;
    println!("Config: {config}");
    Ok(())
}

No error enum needed. No impl From. The ? operator converts any std::error::Error into anyhow::Error.

This page matters most for CLI authors, backend engineers, and Rust learners trying to understand when anyhow is a shortcut versus when it becomes the wrong abstraction.


When Should You Use anyhow?

Use anyhow when the main job of your code is to propagate errors upward cleanly, not to expose a typed error contract to downstream callers.

anyhow is a strong fit when:

  • you are writing a binary crate, CLI, job runner, or service entrypoint
  • you want fast progress without custom error boilerplate
  • top-level logging and operator readability matter more than variant matching
  • lower layers already return many different error types

It is a weaker fit when you are designing a library API, domain model, or SDK where callers need stable, matchable error variants.


How Do You Add Context to Errors?

.context("...") wraps an error with an explanatory message; the original error is preserved as the source.

use anyhow::{Context, Result};
 
fn load_user(id: u64) -> Result<User> {
    let data = std::fs::read_to_string("users.json")
        .context("failed to read users.json")?;
 
    let user: User = serde_json::from_str(&data)
        .with_context(|| format!("failed to parse user with id={id}"))?;
 
    Ok(user)
}

When this error is displayed, the output chains all context messages:

failed to parse user with id=42: expected `,` or `}` at line 5 column 3

Use .context("static message") for fixed strings and .with_context(|| format!(...)) for dynamic messages (the closure avoids allocation when there is no error).


How Do You Create Errors From Scratch?

Use anyhow!("...") for ad-hoc errors and bail!("...") to return an error immediately.

use anyhow::{anyhow, bail, Result};
 
fn validate_age(age: i32) -> Result<()> {
    if age < 0 {
        bail!("age cannot be negative, got {age}");  // returns Err immediately
    }
    if age > 150 {
        return Err(anyhow!("age {age} is unrealistically large"));
    }
    Ok(())
}

bail!("msg") is equivalent to return Err(anyhow!("msg")); a convenience for early returns.


anyhow vs thiserror in 2026

Choose anyhow for application-layer error propagation in 2026; choose thiserror when you need structured, public, typed errors.

anyhowthiserror
PurposePropagate any errorDefine typed errors
Error typeanyhow::Error (dynamic)Your custom enum/struct
Pattern matching❌ Not ergonomic✅ Full match support
? operatorWorks with any errorWorks after From impls
Best forBinaries, CLI, application glueLibraries, domain models
Downcasterr.downcast_ref::<SomeError>()Not needed

A common pattern is still the best one in 2026: define domain errors with thiserror in library crates, then use anyhow in the binary crate that calls them.


How Do You Downcast an anyhow::Error?

anyhow::Error preserves the original error type and supports downcasting when you need to inspect it.

use anyhow::Result;
 
fn handle(err: anyhow::Error) {
    if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
        eprintln!("IO error: kind={:?}", io_err.kind());
    } else {
        eprintln!("Other error: {err}");
    }
}

Downcasting is an escape hatch; if you find yourself doing it often, consider thiserror for that error path instead.


Why Does anyhow Matter Professionally?

anyhow matters because production Rust work is not only about perfect type modeling; it is also about choosing when not to over-engineer error plumbing.

Strong Rust engineers know both sides of the tradeoff. They know when typed errors are the right API design, and they know when a binary or service layer simply needs fast, readable, contextual error propagation. anyhow is one of the clearest examples of Rust maturity being about judgment, not only about using the most explicit type at every layer.


Frequently Asked Questions

You can, but it is not recommended. Library callers receive anyhow::Error and cannot match on specific error cases without downcasting. Use thiserror to expose typed errors to your callers. Reserve anyhow for the top-level application layer.

Use {:#} (alternate display) or the chain() iterator:

eprintln!("Error: {:#}", err);  // prints all context layers
for cause in err.chain() {
    eprintln!("  caused by: {cause}");
}

anyhow::Error is one pointer (8 bytes on 64-bit). It box-allocates the inner error; there is a heap allocation per error value. In the happy path (no error), there is zero overhead. For error-heavy hot paths, typed errors avoid the allocation.

anyhow::Result<T> is a type alias for Result<T, anyhow::Error>. It is a convenience; you can still use std::result::Result<T, anyhow::Error> directly.


Sources


  • thiserror: Define typed errors that anyhow can wrap
  • Result: The Result<T, E> type anyhow plugs into
  • Try Operator: The ? operator central to anyhow usage
  • Error Handling: Rust's broader error handling philosophy

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?