What is the difference between anyhow::Context and thiserror::Error for enriching error information?

anyhow::Context adds contextual information to errors at propagation time through the ? operator, creating a chain of error messages that explain what operation failed and why. thiserror::Error derives structured error types with the #[source] attribute to capture underlying causes as typed fields, enabling programmatic access to error sources. The key difference: Context is for ad-hoc human-readable context added during error propagation (library/application boundary), while thiserror::Error is for defining typed error enums with structured source relationships (library API design).

anyhow::Context: Adding Context During Propagation

use anyhow::{Context, Result};
 
fn read_config(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .context(format!("Failed to read config from {}", path))
}
 
fn parse_config(content: &str) -> Result<Config> {
    toml::from_str(content)
        .context("Failed to parse config TOML")
}
 
fn load_config(path: &str) -> Result<Config> {
    let content = read_config(path)
        .context("Could not load configuration")?;
    
    parse_config(&content)
        .context("Invalid configuration format")
}
 
fn main() -> Result<()> {
    let config = load_config("config.toml")?;
    Ok(())
}
 
// Error chain when config file doesn't exist:
// Error: Could not load configuration
// Caused by:
//     0: Failed to read config from config.toml
//     1: No such file or directory (os error 2)

Context wraps errors with descriptive messages, building a causal chain that explains the failure path.

thiserror::Error: Defining Typed Error Sources

use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("Failed to read config file: {path}")]
    ReadFailed {
        path: String,
        #[source]
        source: std::io::Error,
    },
    
    #[error("Invalid config format")]
    ParseFailed {
        #[source]
        source: toml::de::Error,
    },
}
 
impl From<std::io::Error> for ConfigError {
    fn from(err: std::io::Error) -> Self {
        ConfigError::ReadFailed {
            path: String::new(),
            source: err,
        }
    }
}
 
// The #[source] attribute marks which field holds the underlying error
// This allows programmatic access via std::error::Error::source()

thiserror::Error defines structured error types where source relationships are explicit fields.

Fundamental Design Difference

// anyhow::Context: Ad-hoc context added at call site
 
use anyhow::Context;
 
fn process_file(path: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)
        .context(format!("Reading {}", path))?;
    
    let data: Data = serde_json::from_str(&content)
        .context("Parsing JSON")?;
    
    validate(&data)
        .context("Validating data")?;
    
    Ok(())
}
 
// thiserror::Error: Pre-defined error structure
 
use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum ProcessError {
    #[error("Failed to read {path}")]
    ReadError {
        path: String,
        #[source]
        source: std::io::Error,
    },
    
    #[error("Failed to parse JSON")]
    ParseError {
        #[source]
        source: serde_json::Error,
    },
    
    #[error("Validation failed")]
    ValidationError {
        #[source]
        source: ValidationError,
    },
}
 
fn process_file(path: &str) -> Result<(), ProcessError> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| ProcessError::ReadError {
            path: path.to_string(),
            source: e,
        })?;
    
    let data: Data = serde_json::from_str(&content)
        .map_err(ProcessError::ParseError)?;
    
    validate(&data)
        .map_err(ProcessError::ValidationError)?;
    
    Ok(())
}

Context adds context dynamically at each error site; thiserror requires defining error variants upfront.

Context Propagation with ? Operator

use anyhow::{Context, Result};
 
fn main() -> Result<()> {
    // Context is designed to work with ? operator
    // It wraps the error before propagating
    
    let file = std::fs::File::open("data.txt")
        .context("Opening data file")?;  // Wraps io::Error
    
    let content = std::fs::read_to_string("data.txt")
        .context("Reading file content")?;
    
    let config: Config = toml::from_str(&content)
        .context("Parsing TOML config")?;
    
    // Each context creates a layer in the error chain
    
    Ok(())
}
 
// If the file doesn't exist:
// Error: Opening data file
// Caused by:
//     0: Reading file content
//     1: No such file or directory (os error 2)

The ? operator combined with .context() builds a stack trace of what went wrong.

thiserror Source Chain for Programmatic Access

use thiserror::Error;
use std::error::Error;
 
#[derive(Debug, Error)]
pub enum AppError {
    #[error("Database connection failed")]
    DatabaseError {
        #[source]
        source: sql::Error,
    },
    
    #[error("User not found: {id}")]
    UserNotFound {
        id: u64,
        #[source]
        source: DatabaseError,
    },
}
 
fn main() {
    let error: Box<dyn Error> = AppError::UserNotFound {
        id: 42,
        source: AppError::DatabaseError {
            source: sql::Error::ConnectionFailed,
        },
    };
    
    // Programmatic access to source chain
    let mut current: Option<&dyn Error> = Some(error.as_ref());
    while let Some(err) = current {
        println!("Error: {}", err);
        current = err.source();
    }
    
    // Output:
    // Error: User not found: 42
    // Error: Database connection failed
    // Error: connection failed
}
 
// This enables:
// - Error matching on specific types
// - Structured logging/monitoring
// - Conditional error handling

thiserror enables traversing the error chain programmatically via std::error::Error::source().

Context for Application Code

use anyhow::{Context, Result};
 
// anyhow is ideal for applications where you want rich error messages
// without defining custom error types
 
fn run_server() -> Result<()> {
    let config = load_config("config.toml")
        .context("Failed to initialize server configuration")?;
    
    let listener = bind_socket(&config.bind_address)
        .context("Failed to bind server socket")?;
    
    let database = connect_db(&config.database_url)
        .context("Failed to connect to database")?;
    
    serve_requests(listener, database)
        .context("Server error")?;
    
    Ok(())
}
 
// Error output shows the full context chain:
// Error: Server error
// Caused by:
//     0: Failed to connect to database
//     1: Connection refused (os error 111)

anyhow::Context excels in application code where context messages help debug issues.

thiserror for Library APIs

use thiserror::Error;
 
// thiserror is ideal for libraries where consumers need to match on errors
 
#[derive(Debug, Error)]
pub enum DatabaseError {
    #[error("Connection failed to {host}:{port}")]
    ConnectionFailed {
        host: String,
        port: u16,
        #[source]
        source: std::io::Error,
    },
    
    #[error("Query execution failed")]
    QueryError {
        #[source]
        source: sql::Error,
    },
    
    #[error("Transaction rolled back")]
    TransactionError {
        #[source]
        source: Box<Self>,
    },
    
    #[error("Pool exhausted: no available connections")]
    PoolExhausted,
}
 
// Consumers can match on specific error variants:
fn handle_db_error(err: DatabaseError) {
    match err {
        DatabaseError::ConnectionFailed { host, port, .. } => {
            println!("Retry connection to {}:{}", host, port);
        }
        DatabaseError::PoolExhausted => {
            println!("Wait for available connection");
        }
        _ => {
            println!("Other database error: {}", err);
        }
    }
}

thiserror defines typed error APIs that library consumers can match on.

Combining Context with Error Variants

use anyhow::{Context, Result};
use thiserror::Error;
 
// Library defines typed errors with thiserror
#[derive(Debug, Error)]
pub enum ParseError {
    #[error("Invalid syntax at line {line}")]
    SyntaxError { line: usize },
    
    #[error("Unexpected token: expected {expected}, got {actual}")]
    UnexpectedToken { expected: String, actual: String },
    
    #[error("IO error")]
    Io {
        #[source]
        source: std::io::Error,
    },
}
 
// Application adds context with anyhow
fn parse_config_file(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .context(format!("Reading config file: {}", path))
        .map_err(|e| ParseError::Io { source: e.into() })?;
    
    let config = parse_config(&content)
        .context("Parsing configuration")
        .map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;
    
    Ok(config)
}

Use thiserror for library error types, anyhow::Context in application code.

Context with anyhow::anyhow! Macro

use anyhow::{anyhow, Context, Result};
 
fn validate_age(age: i32) -> Result<()> {
    if age < 0 {
        return Err(anyhow!("Age cannot be negative: {}", age));
    }
    if age > 150 {
        return Err(anyhow!("Age seems unrealistic: {}", age));
    }
    Ok(())
}
 
fn process_user(name: &str, age: i32) -> Result<()> {
    validate_age(age)
        .context(format!("Validating age for user {}", name))?;
    
    Ok(())
}
 
fn main() -> Result<()> {
    process_user("Alice", -5)?;
    Ok(())
}
 
// Error output:
// Error: Validating age for user Alice
// Caused by:
//     0: Age cannot be negative: -5

Combine anyhow! for creating errors and .context() for adding propagation context.

thiserror source vs backtrace

use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum ApiError {
    #[error("HTTP request failed")]
    Http {
        #[source]
        source: reqwest::Error,
    },
    
    #[error("Serialization failed")]
    Serialization {
        #[source]
        source: serde_json::Error,
    },
    
    #[error("Business logic error: {message}")]
    Business {
        message: String,
        // source is optional - not all errors have underlying causes
    },
}
 
// #[source] creates a causal chain accessible via Error::source()
// This is different from a backtrace (call stack)
// The source chain is about *what* caused the error, not *where*
 
// anyhow adds both context (what) and can include backtraces (where)
// when RUST_BACKTRACE=1 is set

#[source] defines causal relationships; anyhow::Context provides both context and optional backtraces.

Error Display and Debug Output

use anyhow::{Context, Result};
use thiserror::Error;
 
// anyhow error display:
fn test_anyhow() -> Result<()> {
    std::fs::read_to_string("nonexistent.txt")
        .context("Loading file")?;
    Ok(())
}
 
// Display: "Loading file"
// Debug: Full chain with "Caused by:" sections
// (anyhow::Error implements both Display and Debug differently)
 
// thiserror error display:
#[derive(Debug, Error)]
pub enum MyError {
    #[error("File not found: {path}")]
    FileNotFound { path: String },
}
 
// Display: Uses #[error(...)] attribute format
// Debug: Uses derive Debug format (or can be customized)
// You control the exact message format with #[error("...")]
 
// anyhow: Less control, automatic context chaining
// thiserror: Full control over error message format

anyhow generates context chains automatically; thiserror gives you explicit control over error messages.

Downcasting for Error Matching

use thiserror::Error;
use std::error::Error;
use std::io;
 
#[derive(Debug, Error)]
pub enum MyError {
    #[error("IO error")]
    Io(#[source] io::Error),
    
    #[error("Parse error")]
    Parse(String),
}
 
fn handle_error(err: Box<dyn Error>) {
    // With thiserror, you can downcast to match specific types
    if let Some(my_err) = err.downcast_ref::<MyError>() {
        match my_err {
            MyError::Io(e) => println!("IO error: {}", e),
            MyError::Parse(s) => println!("Parse error: {}", s),
        }
    }
    
    // You can also access the source chain
    if let Some(source) = err.source() {
        println!("Caused by: {}", source);
    }
}
 
// anyhow also supports downcasting
use anyhow::anyhow;
 
fn handle_anyhow_error(err: anyhow::Error) {
    if let Some(io_err) = err.downcast_ref::<io::Error>() {
        println!("Got IO error: {}", io_err);
    }
}

Both support downcasting; thiserror makes the source relationship explicit via #[source].

Memory and Performance Characteristics

use anyhow::Context;
use thiserror::Error;
 
// anyhow::Error:
// - Heap-allocated (dyn Error inside)
// - Can hold any error type
// - Contains optional backtrace
// - Slightly larger memory footprint
// - Great for propagating diverse errors
 
// thiserror-defined errors:
// - Stack-allocated (enum variants)
// - Size depends on variant data
// - No backtrace overhead
// - Predictable size for matching
// - Better for libraries with specific error types
 
// Example size comparison:
#[derive(Debug, Error)]
pub enum LibraryError {
    #[error("IO error")]
    Io(#[source] std::io::Error),  // Size: std::io::Error + enum discriminant
    
    #[error("Invalid input")]
    InvalidInput,  // Size: just discriminant (small)
}
 
// anyhow::Error can hold any of these plus context strings
// Size overhead: Box<dyn Error> + Option<Backtrace> + context strings

anyhow::Error is heap-allocated and can hold any error; thiserror errors are sized at compile time.

Practical Usage Recommendations

// Application code: Use anyhow
 
use anyhow::{Context, Result};
 
fn run_application() -> Result<()> {
    let config = load_config()
        .context("Failed to load configuration")?;
    
    let server = start_server(&config)
        .context("Failed to start server")?;
    
    serve_requests(server)
        .context("Server error")?;
    
    Ok(())
}
 
// Library code: Use thiserror
 
use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("IO error reading {path}")]
    Io {
        path: String,
        #[source]
        source: std::io::Error,
    },
    
    #[error("Invalid format")]
    Format {
        #[source]
        source: toml::de::Error,
    },
}
 
pub fn load_config(path: &str) -> Result<Config, ConfigError> {
    // Library returns typed errors
}
 
// Application consuming library:
 
fn app_load_config(path: &str) -> anyhow::Result<Config> {
    load_config(path)
        .context(format!("Loading config from {}", path))
        .map_err(anyhow::Error::from)
}

Library: Use thiserror for typed error APIs that consumers can match on. Application: Use anyhow::Context to add rich context for debugging.

Context on Option Types

use anyhow::{Context, Result};
 
fn main() -> Result<()> {
    // Context works on Option too (not just Result)
    
    let value: Option<i32> = None;
    
    let value = value
        .context("Expected a value but got None")?;
    
    // This converts None to an error with the context message
    
    // Useful for:
    // - HashMap lookups
    // - Optional configuration
    // - Finding items
    
    let config: HashMap<&str, &str> = HashMap::new();
    let db_url = config
        .get("database_url")
        .context("Missing 'database_url' in configuration")?;
    
    Ok(())
}
 
// Error: Missing 'database_url' in configuration

.context() works on Option<T> to convert None into an error with a message.

thiserror Backtrace Support

use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum MyError {
    #[error("Operation failed")]
    Failed {
        #[source]
        source: std::io::Error,
        #[backtrace]  // Requires nightly or feature flag
        backtrace: std::backtrace::Backtrace,
    },
}
 
// Note: Backtrace in thiserror requires std::backtrace support
// This is separate from the source chain
// anyhow::Context automatically captures backtraces when RUST_BACKTRACE=1

thiserror supports #[backtrace] for explicit backtrace capture; anyhow does this automatically.

Synthesis

anyhow::Context:

  • Adds context at propagation time via .context()
  • Creates human-readable error chains automatically
  • Works with ? operator for ergonomic error propagation
  • Includes optional backtrace support
  • Returns anyhow::Error (boxed trait object)
  • Best for: Application code, prototypes, CLI tools

thiserror::Error:

  • Defines structured error types with #[derive(Error)]
  • #[source] attribute marks underlying error fields
  • Enables programmatic access via Error::source()
  • Explicit control over error message format
  • Returns typed error enums/structs
  • Best for: Library APIs, typed error handling

Key comparison:

Aspect anyhow::Context thiserror::Error
When to add context Runtime (propagation) Compile time (definition)
Error type anyhow::Error (boxed) User-defined (sized)
Source access Via Error::source() Via #[source] field
Message control Automatic chaining Explicit via #[error(...)]
Downcasting Supported Supported (with enum matching)
Best for Applications Libraries

The fundamental insight: anyhow::Context is about enriching errors with context during propagation—you add information about what you were trying to do when an error occurred. thiserror::Error is about defining structured error types with explicit source relationships—you declare the shape of your errors upfront. Use thiserror when you want consumers to match on specific error variants; use anyhow::Context when you want rich error messages without defining error types.