How does anyhow::Context::with_context differ from context for lazy context evaluation?

context eagerly evaluates its argument immediately when called, while with_context accepts a closure that's only evaluated if an error actually occurs. This distinction matters when constructing context strings is expensive—with_context avoids the overhead of building context for successful results, making it the better choice when context computation involves string formatting, data lookups, or other non-trivial operations. For simple static strings, context is slightly more direct but functionally equivalent.

Basic context Usage

use anyhow::{Context, Result};
 
fn read_config() -> Result<String> {
    let path = "config.toml";
    
    // context is called immediately, string is always created
    let contents = std::fs::read_to_string(path)
        .context(format!("Failed to read config from {}", path))?;
    
    Ok(contents)
}
 
fn main() {
    match read_config() {
        Ok(config) => println!("Config: {}", config),
        Err(e) => eprintln!("Error: {}", e),
    }
}

context immediately evaluates its argument, even if no error occurs.

Basic with_context Usage

use anyhow::{Context, Result};
 
fn read_config() -> Result<String> {
    let path = "config.toml";
    
    // with_context takes a closure, only called if error occurs
    let contents = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read config from {}", path))?;
    
    Ok(contents)
}
 
fn main() {
    match read_config() {
        Ok(config) => println!("Config: {}", config),
        Err(e) => eprintln!("Error: {}", e),
    }
}

with_context only invokes the closure when an error needs context.

Eager vs Lazy Evaluation

use anyhow::{Context, Result};
 
fn eager_vs_lazy() -> Result<()> {
    // Eager evaluation with context
    let result: Result<i32> = Ok(42);
    
    // This string is ALWAYS created, even though no error
    result.context("An error occurred")?;
    
    // Lazy evaluation with with_context
    let result: Result<i32> = Ok(42);
    
    // This closure is NEVER called because no error
    result.with_context(|| {
        println!("This won't print!");
        format!("An error occurred")
    })?;
    
    Ok(())
}

The key difference: context always creates the string; with_context only creates it on error.

Function Signatures

use anyhow::Context;
 
// Simplified signatures:
 
// context: eager evaluation
// fn context<D>(self, context: D) -> Result<T, Error>
// where D: Display + Send + Sync + 'static
//
// Takes the context value directly - evaluated immediately
 
// with_context: lazy evaluation  
// fn with_context<F, D>(self, f: F) -> Result<T, Error>
// where F: FnOnce() -> D, D: Display + Send + Sync + 'static
//
// Takes a closure - only called if error occurs
 
fn signature_example() {
    let result: Result<i32> = Ok(1);
    
    // context takes Display value
    result.context("Static message");
    result.context(format!("Dynamic {}", 123));
    
    // with_context takes FnOnce() -> Display
    result.with_context(|| "Static message".to_string());
    result.with_context(|| format!("Dynamic {}", 123));
}

context takes a Display value; with_context takes FnOnce() -> Display.

Performance Implications

use anyhow::{Context, Result};
 
fn expensive_context() -> Result<()> {
    let path = "data.txt";
    
    // context: expensive computation happens even on success
    let data = std::fs::read_to_string(path)
        .context(build_expensive_context(path))?;
    
    Ok(())
}
 
fn build_expensive_context(path: &str) -> String {
    // This is called even when read_to_string succeeds!
    // Expensive operations: database queries, complex formatting, etc.
    format!("Failed to read {}. Additional info: {:?}", 
            path, 
            std::fs::metadata(path).ok())  // Another file operation!
}
 
fn with_context_efficient() -> Result<()> {
    let path = "data.txt";
    
    // with_context: closure only executed on error
    let data = std::fs::read_to_string(path)
        .with_context(|| {
            // This only runs if read_to_string fails
            build_expensive_context(path)
        })?;
    
    Ok(())
}

Use with_context when building context is expensive and errors are rare.

String Formatting Overhead

use anyhow::{Context, Result};
 
fn formatting_overhead() -> Result<()> {
    let filename = "example.txt";
    let line_number = 42;
    let column = 10;
    
    // context: formats string immediately
    // Even if operation succeeds, format! is called
    operation()
        .context(format!("Error at {}:{}:{} in {}", 
                        line_number, column, filename, filename))?;
    
    // with_context: closure only called on error
    // If operation succeeds, no formatting happens
    operation()
        .with_context(|| format!("Error at {}:{}:{} in {}",
                                line_number, column, filename, filename))?;
    
    Ok(())
}
 
fn operation() -> Result<()> {
    Ok(())
}

String formatting is cheap but not free; with_context avoids it on success.

Multiple Error Contexts

use anyhow::{Context, Result};
 
fn chained_operations() -> Result<()> {
    let config_path = "config.toml";
    let data_path = "data.bin";
    
    // Each context is eager
    let config = std::fs::read_to_string(config_path)
        .context(format!("Failed to read {}", config_path))?;
    
    let data = std::fs::read(data_path)
        .context(format!("Failed to read {}", data_path))?;
    
    // If both succeed, both format! calls were made
    Ok(())
}
 
fn chained_with_context() -> Result<()> {
    let config_path = "config.toml";
    let data_path = "data.bin";
    
    // Each with_context is lazy
    let config = std::fs::read_to_string(config_path)
        .with_context(|| format!("Failed to read {}", config_path))?;
    
    let data = std::fs::read(data_path)
        .with_context(|| format!("Failed to read {}", data_path))?;
    
    // If both succeed, neither closure was called
    Ok(())
}

With multiple operations, with_context accumulates savings.

Static Strings: No Difference

use anyhow::{Context, Result};
 
fn static_strings() -> Result<()> {
    // For static strings, context is perfectly fine
    // No allocation, no computation
    
    operation()
        .context("Static error message")?;  // &str - cheap
    
    operation()
        .with_context(|| "Static error message")?;  // Also fine, slightly more syntax
    
    // Both are essentially equivalent for static strings
    // context is slightly cleaner for this case
    
    Ok(())
}
 
fn operation() -> Result<()> {
    Ok(())
}

For static strings, context is simpler and equally efficient.

Dynamic Context from Variables

use anyhow::{Context, Result};
 
fn dynamic_context() -> Result<()> {
    let user_id = 12345;
    let request_id = "abc-123-def";
    
    // context: captures variables by value immediately
    operation()
        .context(format!("Failed for user {} request {}", user_id, request_id))?;
    
    // with_context: captures variables, only uses them on error
    operation()
        .with_context(|| format!("Failed for user {} request {}", user_id, request_id))?;
    
    Ok(())
}
 
fn operation() -> Result<()> {
    Ok(())
}

Both capture variables; with_context delays the formatting.

Context with External State

use anyhow::{Context, Result};
 
fn with_external_state() -> Result<()> {
    let mut counter = 0;
    
    for i in 0..100 {
        // context: would need to capture current state
        // with_context: closure captures by reference
        
        operation()
            .with_context(|| {
                counter += 1;  // Can access external state
                format!("Error in iteration {}", i)
            })?;
    }
    
    Ok(())
}
 
fn operation() -> Result<()> {
    Ok(())
}

with_context closures can access external state when errors occur.

Database Context Example

use anyhow::{Context, Result};
 
struct Database {
    // Simulated database
}
 
impl Database {
    fn get_user(&self, id: u32) -> Result<String> {
        // Expensive: would query database for context
        Ok(format!("User {}", id))
    }
}
 
fn fetch_user(db: &Database, user_id: u32) -> Result<String> {
    // Imagine this might fail
    let name = lookup_user(user_id)
        .context(db.get_user(user_id)?)?;  // Always queries DB!
    
    Ok(name)
}
 
fn fetch_user_lazy(db: &Database, user_id: u32) -> Result<String> {
    let name = lookup_user(user_id)
        .with_context(|| {
            // Only queries DB if lookup fails
            db.get_user(user_id).unwrap_or_else(|_| format!("user {}", user_id))
        })?;
    
    Ok(name)
}
 
fn lookup_user(id: u32) -> Result<String> {
    Ok(format!("User {}", id))
}

with_context avoids expensive context operations when there's no error.

Nested Context Chains

use anyhow::{Context, Result};
 
fn nested_context() -> Result<()> {
    let file = "config.toml";
    
    // context at each level
    let config = std::fs::read_to_string(file)
        .context(format!("Failed to read {}", file))
        .context("Configuration loading failed")?;
    
    // If read_to_string succeeds, both context strings were created
    Ok(())
}
 
fn nested_with_context() -> Result<()> {
    let file = "config.toml";
    
    // with_context at each level
    let config = std::fs::read_to_string(file)
        .with_context(|| format!("Failed to read {}", file))
        .with_context(|| "Configuration loading failed".to_string())?;
    
    // If read_to_string succeeds, neither closure was called
    Ok(())
}

Nested contexts accumulate overhead; with_context defers all of it.

When to Use context

use anyhow::{Context, Result};
 
fn when_to_use_context() -> Result<()> {
    // Use context when:
    // 1. Context string is static or very cheap
    // 2. Errors are expected to be common
    // 3. Simplicity is preferred
    
    // Static string - perfect for context
    operation().context("Failed to connect")?;
    
    // Very cheap format - also fine
    let id = 42;
    operation().context(format!("Error {}", id))?;
    
    // Simple string concatenation - acceptable
    operation().context("Something went wrong")?;
    
    Ok(())
}
 
fn operation() -> Result<()> {
    Ok(())
}

Use context for static strings or when simplicity matters more than micro-optimization.

When to Use with_context

use anyhow::{Context, Result};
 
fn when_to_use_with_context() -> Result<()> {
    let user_id = 12345;
    let request = Request { id: "abc", path: "/api/data" };
    
    // Use with_context when:
    // 1. Context computation is expensive
    // 2. Errors are rare (hot path)
    // 3. Building context from multiple sources
    
    // Expensive format with multiple variables
    operation()
        .with_context(|| {
            format!(
                "Failed processing request {} at path {} for user {}",
                request.id, request.path, user_id
            )
        })?;
    
    // Context requiring external data
    operation()
        .with_context(|| {
            let timestamp = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap()
                .as_secs();
            format!("Error at timestamp {}", timestamp)
        })?;
    
    Ok(())
}
 
struct Request {
    id: &'static str,
    path: &'static str,
}
 
fn operation() -> Result<()> {
    Ok(())
}

Use with_context for expensive context or when errors are rare.

Real-World Example: File Processing

use anyhow::{Context, Result};
use std::path::Path;
 
fn process_file(path: &Path) -> Result<()> {
    // Expensive context: canonicalize + metadata + formatting
    let canonical = std::fs::canonicalize(path)
        .with_context(|| {
            let abs = std::fs::canonicalize(path.parent().unwrap_or(path))
                .unwrap_or_else(|_| path.to_path_buf());
            let size = std::fs::metadata(path)
                .map(|m| format!("{} bytes", m.len()))
                .unwrap_or_else(|_| "unknown size".to_string());
            format!("Failed to process {} ({})", abs.display(), size)
        })?;
    
    // Process contents...
    Ok(())
}
 
fn process_file_simple(path: &Path) -> Result<()> {
    // Simple context: just the path
    let contents = std::fs::read_to_string(path)
        .context(format!("Failed to read {}", path.display()))?;
    
    // For simple cases, context is fine
    Ok(())
}

Match the approach to the complexity of the context.

Benchmark Considerations

use anyhow::{Context, Result};
 
fn benchmark_context() {
    // Hot path with 1 million iterations
    // Success case (no error)
    
    // context: formats string every time
    for _ in 0..1_000_000 {
        let _: Result<()> = Ok(())
            .context(format!("Error at {}", std::time::Instant::now()));
        // format! called 1 million times
    }
    
    // with_context: closure never called
    for _ in 0..1_000_000 {
        let _: Result<()> = Ok(())
            .with_context(|| format!("Error at {}", std::time::Instant::now()));
        // Closure never called, format! never runs
    }
    
    // In hot paths with rare errors, with_context can be measurably faster
}

In hot paths, with_context can provide measurable performance benefits.

Common Patterns

use anyhow::{Context, Result};
use std::path::Path;
 
fn common_patterns() -> Result<()> {
    let path = Path::new("data.txt");
    
    // Pattern 1: Static message
    std::fs::read_to_string(path)
        .context("Failed to read file")?;
    
    // Pattern 2: Single variable
    std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;
    
    // Pattern 3: Rich context
    std::fs::read_to_string(path)
        .with_context(|| {
            let cwd = std::env::current_dir()
                .map(|p| p.display().to_string())
                .unwrap_or_else(|_| "unknown".to_string());
            format!(
                "Failed to read {}\nWorking directory: {}\nPermissions: {:?}",
                path.display(),
                cwd,
                std::fs::metadata(path).ok()
            )
        })?;
    
    Ok(())
}

Choose the pattern based on context complexity and performance needs.

Comparison Summary

use anyhow::{Context, Result};
 
fn comparison_table() {
    // | Aspect | context | with_context |
    // |--------|---------|--------------|
    // | Evaluation | Eager (immediate) | Lazy (on error) |
    // | Argument | Display value | FnOnce() -> Display |
    // | Overhead | Always | Only on error |
    // | Syntax | Simpler | Slightly more complex |
    // | Use case | Static strings | Expensive context |
    // | Performance | Always pays cost | Success is free |
    
    // For static strings: use context (simpler)
    // For expensive context: use with_context (avoids cost)
}
 
fn example() -> Result<()> {
    let x = 42;
    
    // Static: context is fine
    Ok(()).context("Static message")?;
    
    // Dynamic: with_context is better for hot paths
    Ok(()).with_context(|| format!("Value: {}", x))?;
    
    Ok(())
}

Synthesis

Quick reference:

use anyhow::{Context, Result};
 
fn quick_reference() -> Result<()> {
    let path = "config.toml";
    let user_id = 12345;
    
    // context: Eager evaluation
    // - Takes Display value directly
    // - Evaluates immediately when called
    // - Use for static strings or cheap formats
    Ok(())
        .context("Static message")?;
    
    // with_context: Lazy evaluation
    // - Takes closure that returns Display
    // - Only calls closure if error occurs
    // - Use for expensive context building
    Ok(())
        .with_context(|| format!("Failed for user {}", user_id))?;
    
    // Rule of thumb:
    // - Static string: context
    // - Any formatting: with_context (unless errors are common)
    // - Hot path with rare errors: with_context
    
    Ok(())
}

Key insight: The choice between context and with_context is about when the context string is constructed. context constructs it eagerly, making it suitable for static strings or when errors are expected. with_context defers construction until an error actually occurs, avoiding the overhead entirely in the success case. This matters most in hot paths where successful operations vastly outnumber errors—the lazy evaluation of with_context means zero overhead for the common case. The closure syntax is slightly more verbose, but the performance benefit can be substantial when context building involves formatting, lookups, or other non-trivial operations. As a rule of thumb, use context for static strings and with_context for any computed context, especially in performance-sensitive code.