How does anyhow::Context::context add contextual information to errors without wrapping types?

context adds contextual information by converting errors into anyhow::Error, which stores the original error as a cause and the context string as additional metadata, allowing the error to be displayed with its context without requiring a wrapper type to be manually defined. The Context trait is implemented for Result<T, E> where E: StdError + Send + Sync + 'static, enabling .context() to be called on any Result with a standard error type.

Basic Context Usage

use anyhow::{Context, Result};
use std::fs::File;
 
fn basic_context() -> Result<()> {
    // Without context:
    let file = File::open("config.toml")?;
    // Error: No such file or directory (os error 2)
    
    // With context:
    let file = File::open("config.toml")
        .context("failed to open configuration file")?;
    // Error: failed to open configuration file
    // Caused by:
    //     No such file or directory (os error 2)
    
    Ok(())
}

The context message is prepended to the error chain when displayed.

The Context Trait

use anyhow::Context;
 
// The Context trait is defined as:
// pub trait Context<T, E> {
//     fn context<C>(self, context: C) -> Result<T, Error>
//     where
//         C: Display + Send + Sync + 'static;
//     
//     fn with_context<C, F>(self, f: F) -> Result<T, Error>
//     where
//         C: Display + Send + Sync + 'static,
//         F: FnOnce() -> C;
// }
//
// It's implemented for Result<T, E> where E: StdError + Send + Sync + 'static
 
fn trait_explanation() -> anyhow::Result<()> {
    // context() converts the error into anyhow::Error
    // The context string is stored alongside the error
    
    // Works with any error type implementing std::error::Error
    std::fs::read_to_string("file.txt")
        .context("failed to read file")?;
    
    // The original error type is preserved inside anyhow::Error
    // It can be retrieved with error.source() or downcast
    
    Ok(())
}

Context is implemented broadly for Result types, enabling context on any standard error.

How Errors Are Stored

use anyhow::{Context, Error};
 
fn error_storage() {
    // When .context() is called:
    // 1. The original error is wrapped in anyhow::Error
    // 2. The context string is stored as "context" for display
    // 3. The resulting type is Result<T, anyhow::Error>
    
    // anyhow::Error is a thin wrapper around a boxed error trait object
    // It stores:
    // - The original error (as dyn StdError)
    // - The context message (as dyn Display + Send + Sync)
    // - Backtrace (if RUST_BACKTRACE is set)
    
    let result: Result<String, std::io::Error> = std::fs::read_to_string("missing.txt");
    let result_with_context = result.context("loading configuration");
    
    // result_with_context is now Result<String, anyhow::Error>
    // The std::io::Error is preserved inside
    // The context "loading configuration" is attached
}

anyhow::Error stores both the original error and context for later display.

Context vs With Context

use anyhow::{Context, Result};
 
fn context_vs_with_context() -> Result<()> {
    // context() takes a static string or value
    let _file = std::fs::File::open("config.toml")
        .context("failed to open config file")?;
    
    // with_context() takes a closure, evaluated only on error
    let filename = "config.toml";
    let _file = std::fs::File::open(filename)
        .with_context(|| format!("failed to open file: {}", filename))?;
    
    // with_context is useful for:
    // 1. Expensive operations that should only run on error
    // 2. Dynamic values that need to be captured
    // 3. Computed context strings
    
    // The closure is only called if the Result is Err
    // This avoids allocation for successful paths
    
    Ok(())
}

with_context defers context computation until an error occurs.

Error Chain Display

use anyhow::{Context, Result};
 
fn error_chain() -> Result<()> {
    std::fs::read_to_string("config.toml")
        .context("reading configuration")?;
    
    Ok(())
}
 
fn demonstrate_chain() {
    match error_chain() {
        Ok(()) => {}
        Err(e) => {
            // Display shows context + cause
            println!("Error: {}", e);
            // Error: reading configuration
            // Caused by:
            //     No such file or directory (os error 2)
            
            // Debug shows full chain
            println!("Error: {:?}", e);
            // Error: reading configuration
            // 
            // Caused by:
            //     No such file or directory (os error 2)
            
            // Chain traversal
            let mut source = e.source();
            while let Some(cause) = source {
                println!("Caused by: {}", cause);
                source = cause.source();
            }
        }
    }
}

Context appears in both Display and Debug output, with the original error as a cause.

Nested Context

use anyhow::{Context, Result};
 
fn nested_context() -> Result<()> {
    std::fs::read_to_string("config.toml")
        .context("loading config file")?
        .parse::<toml::Value>()
        .context("parsing config content")?;
    
    Ok(())
}
 
// Multiple context layers create an error chain:
// Error: parsing config content
// Caused by:
//     0: loading config file
//     1: expected equals, found newline at line 5
//     2: No such file or directory (os error 2)
// 
// Note: The chain includes all contexts and the original error
 
fn load_config() -> Result<()> {
    let content = std::fs::read_to_string("config.toml")
        .context("failed to read configuration file")?;
    
    let config: Config = toml::from_str(&content)
        .context("failed to parse configuration")?;
    
    validate_config(&config)
        .context("invalid configuration")?;
    
    Ok(())
}

Each .context() call adds a layer to the error chain.

No Wrapping Type Required

use anyhow::{Context, Result};
 
// Traditional error handling with custom types:
mod traditional {
    use std::fmt;
    
    #[derive(Debug)]
    pub enum ConfigError {
        Io(std::io::Error),
        Parse(String),
    }
    
    impl fmt::Display for ConfigError {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            match self {
                ConfigError::Io(e) => write!(f, "IO error: {}", e),
                ConfigError::Parse(s) => write!(f, "Parse error: {}", s),
            }
        }
    }
    
    impl std::error::Error for ConfigError {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            match self {
                ConfigError::Io(e) => Some(e),
                ConfigError::Parse(_) => None,
            }
        }
    }
    
    // This requires defining wrapper types for every context
}
 
// With anyhow, no wrapper needed:
fn with_anyhow() -> Result<()> {
    std::fs::read_to_string("config.toml")
        .context("reading configuration")?;
    Ok(())
}
 
// The context is added dynamically
// No enum variants or Display impl needed

context avoids boilerplate wrapper types while providing rich error information.

Working with Error

use anyhow::{Context, Error, Result};
 
fn working_with_error() -> Result<()> {
    let result: Result<()> = Err(Error::msg("something failed"));
    
    // Error::msg creates an error from a message
    let err = Error::msg("custom error message");
    
    // context works on Result<_, Error> too
    let result: Result<()> = Err(err)
        .context("additional context");
    
    // But context primarily converts non-anyhow errors
    // to anyhow::Error with attached context
    
    Ok(())
}
 
fn downcasting() {
    let result: Result<String, std::io::Error> = 
        Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file missing"));
    
    let err = result.context("loading data").unwrap_err();
    
    // Downcast to original error type
    if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
        println!("IO error kind: {:?}", io_err.kind());
    }
    
    // The original error is preserved inside anyhow::Error
}

anyhow::Error preserves the original error for downcasting.

Context in Libraries vs Applications

use anyhow::{Context, Result};
 
// Libraries should use Result<T, Box<dyn StdError>> or similar
// And return concrete error types
 
// Library code:
mod library {
    use std::io;
    
    pub fn load_config(path: &str) -> Result<String, io::Error> {
        std::fs::read_to_string(path)
    }
}
 
// Application code:
fn application() -> Result<()> {
    // Applications use anyhow and .context() to add context
    let config = library::load_config("config.toml")
        .context("failed to load application configuration")?;
    
    // Libraries return specific errors
    // Applications add context with anyhow
    
    Ok(())
}
 
// Guideline:
// - Libraries: return Result<T, ConcreteError>
// - Applications: use anyhow for error handling and .context()

Use anyhow in applications; libraries should return specific error types.

Performance Considerations

use anyhow::{Context, Result};
 
fn performance() -> Result<()> {
    // context() has minimal overhead on success
    // The string is stored in the Result type
    
    // with_context() is more efficient for expensive context:
    let _file = std::fs::File::open("config.toml")
        .with_context(|| {
            // This closure only runs on error
            // Expensive operation here won't affect success path
            format!("failed to open: {:?}", std::env::current_dir())
        })?;
    
    // For hot paths, with_context avoids:
    // - String allocation on success
    // - Computation of context string
    
    // context() with static strings is very cheap
    let _file = std::fs::File::open("data.txt")
        .context("opening data file")?;  // Static string, minimal cost
    
    Ok(())
}

with_context defers expensive context computation until an error occurs.

Combining Multiple Error Types

use anyhow::{Context, Result};
use std::num::ParseIntError;
 
fn combine_errors() -> Result<()> {
    // Different error types unified through context
    let content = std::fs::read_to_string("config.txt")
        .context("reading file")?;  // io::Error -> anyhow::Error
    
    let number: i32 = content.trim().parse()
        .context("parsing number")?;  // ParseIntError -> anyhow::Error
    
    let addr: std::net::SocketAddr = "invalid".parse()
        .context("parsing address")?;  // AddrParseError -> anyhow::Error
    
    // All different error types converted to anyhow::Error
    // With context attached to each
    
    Ok(())
}
 
// This unifies error types without defining a custom enum

context converts any std::error::Error into anyhow::Error with attached context.

Error Sources and Chains

use anyhow::{Context, Result};
 
fn chain_example() -> Result<()> {
    std::fs::read_to_string("missing.txt")
        .context("step 1: reading file")?
        .parse::<i32>()
        .context("step 2: parsing number")?;
    Ok(())
}
 
fn examine_chain() {
    if let Err(e) = chain_example() {
        // e is anyhow::Error
        
        // The chain of causes
        let mut current: Option<&dyn std::error::Error> = Some(&e);
        let mut depth = 0;
        
        while let Some(err) = current {
            println!("{}: {}", depth, err);
            current = err.source();
            depth += 1;
        }
        
        // Note: anyhow's Error doesn't use the standard source chain
        // exactly the same way. The context is stored separately.
        
        // Use error chain iterator:
        for cause in e.chain() {
            println!("Caused by: {}", cause);
        }
    }
}

The chain() method provides access to all error causes.

Backtraces

use anyhow::{Context, Result};
 
fn backtraces() -> Result<()> {
    // Backtraces are captured automatically if RUST_BACKTRACE=1
    
    std::fs::read_to_string("missing.txt")
        .context("loading file")?;
    
    // The backtrace shows where .context() was called
    // This helps identify where the error originated
    
    Ok(())
}
 
// Backtrace is stored inside anyhow::Error
// Access with error.backtrace()
 
fn print_backtrace() {
    if let Err(e) = backtraces() {
        if let Some(bt) = e.backtrace() {
            println!("Backtrace:\n{}", bt);
        }
    }
}

Backtraces are captured when RUST_BACKTRACE is enabled.

Common Patterns

use anyhow::{Context, Result};
 
fn common_patterns() -> Result<()> {
    // Pattern 1: Context at function boundaries
    fn read_config(path: &str) -> Result<String> {
        std::fs::read_to_string(path)
            .context(format!("loading config from {}", path))
    }
    
    // Pattern 2: Multiple contexts for different operations
    let data = std::fs::read_to_string("input.txt")
        .context("reading input file")?;
    let parsed: Vec<i32> = data.lines()
        .map(|line| line.parse())
        .collect::<Result<Vec<_>, _>>()
        .context("parsing input data")?;
    
    // Pattern 3: Context with dynamic values
    let filename = "config.json";
    let content = std::fs::read_to_string(filename)
        .with_context(|| format!("reading file: {}", filename))?;
    
    // Pattern 4: Early return with context
    fn find_user(id: u64) -> Result<User> {
        database::find_user(id)
            .with_context(|| format!("user {} not found", id))
    }
    
    Ok(())
}
 
struct User;
mod database {
    use super::User;
    use anyhow::Result;
    pub fn find_user(_id: u64) -> Result<User> {
        anyhow::bail!("not found")
    }
}

Common patterns use context at function boundaries and with dynamic values.

Synthesis

Quick reference:

Method Signature When to Use
.context() context<C>(msg: C) -> Result<T, Error> Static strings, simple context
.with_context() with_context<C, F>(f: F) -> Result<T, Error> Dynamic context, expensive computation

How context works internally:

// Simplified mental model of anyhow::Error:
// 
// struct Error {
//     inner: Box<ErrorImpl>,
// }
// 
// struct ErrorImpl {
//     source: Option<Box<dyn StdError + Send + Sync>>,
//     message: Option<String>,  // or context
//     backtrace: Option<Backtrace>,
// }
//
// When .context() is called:
// 1. The original error is boxed as source
// 2. The context string becomes the message
// 3. The error chain is: context -> source

Key insight: anyhow::Context::context adds contextual information without requiring wrapper types by converting errors into anyhow::Error, which stores both the original error and the context string internally. The Context trait is implemented for Result<T, E> where E: StdError + Send + Sync + 'static, enabling .context() calls on virtually any Result. When the error is displayed, the context appears first, followed by the original error as a cause. This approach provides the ergonomics of custom error wrapper types without requiring boilerplate enum definitions. The .with_context() variant takes a closure that's only evaluated on error, avoiding expensive string formatting on success paths. Context stacks: multiple .context() calls create a chain of context messages, all preserved for debugging. Use context in application code to enrich errors with location and operation information; libraries should return specific error types and let applications add context. The original error remains accessible via downcast_ref::<T>() for type-specific handling, and the full chain is iterable via error.chain(). Backtraces are automatically captured when RUST_BACKTRACE is set, showing where .context() was called.