What is the difference between anyhow::Context and anyhow::with_context for adding context to errors?

anyhow::Context provides two methods for adding contextual information to errors: .context() evaluates its argument immediately regardless of whether an error occurs, while .with_context() takes a closure that's only evaluated when an error actually exists. This distinction matters for performance when the context string is expensive to compute—.context(format!(...)) always allocates and formats, but .with_context(|| format!(...)) only does so on error. Both methods return Result types with the error wrapped in additional context, preserving the original error as the source chain. The choice between them depends on whether the context computation cost matters in the success path.

Basic context Usage

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 main() -> Result<()> {
    let config = read_config("config.toml")?;
    Ok(())
}

.context() always evaluates its argument, even on success.

Basic with_context Usage

use anyhow::{Context, Result};
 
fn read_config(path: &str) -> Result<String> {
    std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read config from {}", path))
}
 
fn main() -> Result<()> {
    let config = read_config("config.toml")?;
    Ok(())
}

.with_context() only evaluates the closure on error.

Eager vs Lazy Evaluation Difference

use anyhow::{Context, Result};
 
fn expensive_context() -> String {
    println!("Computing context...");
    "expensive context string".to_string()
}
 
fn main() -> Result<()> {
    // context: Always evaluates
    let result1: Result<i32> = Ok(42)
        .context(expensive_context());
    // Prints "Computing context..." even though no error
    
    // with_context: Only evaluates on error
    let result2: Result<i32> = Ok(42)
        .with_context(|| expensive_context());
    // Does NOT print - closure never called
    
    let result3: Result<i32> = Err(anyhow::anyhow!("error"))
        .with_context(|| expensive_context());
    // Prints "Computing context..." because error occurred
    
    Ok(())
}

.context() pays the formatting cost always; .with_context() only on error.

Performance Impact Demonstration

use anyhow::{Context, Result};
 
fn main() -> Result<()> {
    let iterations = 1_000_000;
    
    // Using context (eager)
    let start = std::time::Instant::now();
    for _ in 0..iterations {
        let _: Result<i32> = Ok(42)
            .context(format!("Error value: {}", 12345));
    }
    let eager_time = start.elapsed();
    
    // Using with_context (lazy)
    let start = std::time::Instant::now();
    for _ in 0..iterations {
        let _: Result<i32> = Ok(42)
            .with_context(|| format!("Error value: {}", 12345));
    }
    let lazy_time = start.elapsed();
    
    println!("Eager context: {:?}", eager_time);
    println!("Lazy with_context: {:?}", lazy_time);
    // Lazy version is faster because closure never called on success
    
    Ok(())
}

.with_context() avoids work on the happy path.

Static String Context

use anyhow::{Context, Result};
 
fn read_file(path: &str) -> Result<String> {
    // Static strings have no formatting cost
    // context and with_context are equivalent
    std::fs::read_to_string(path)
        .context("Failed to read file")
}
 
fn read_file_lazy(path: &str) -> Result<String> {
    // For static strings, either form works identically
    std::fs::read_to_string(path)
        .with_context(|| "Failed to read file")
}

For static strings, both forms are equivalent.

Dynamic Context with format!

use anyhow::{Context, Result};
 
fn parse_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read config file: {}", path))?;
    
    toml::from_str(&content)
        .with_context(|| format!("Failed to parse config from: {}", path))
}
 
#[derive(Debug)]
struct Config {
    name: String,
}

Use .with_context() with format! to avoid unnecessary formatting.

Context Chain and Error Display

use anyhow::{Context, Result};
 
fn process_data() -> Result<()> {
    let data = std::fs::read_to_string("data.json")
        .context("Failed to read data file")?;
    
    let parsed: serde_json::Value = serde_json::from_str(&data)
        .context("Failed to parse JSON")?;
    
    let name = parsed["name"]
        .as_str()
        .ok_or_else(|| anyhow::anyhow!("Missing name field"))
        .context("Invalid data structure")?;
    
    println!("Name: {}", name);
    Ok(())
}
 
fn main() {
    if let Err(e) = process_data() {
        // Error chain shows all context
        eprintln!("Error: {:?}", e);
        // Error shows full chain:
        // "Invalid data structure"
        // Caused by: "Missing name field"
    }
}

Context chains create a clear error narrative.

Accessing the Error Chain

use anyhow::{Context, Result};
 
fn main() -> Result<()> {
    let result: Result<i32> = Err(anyhow::anyhow!("original error"))
        .context("first context")
        .context("second context");
    
    match result {
        Err(e) => {
            // Display the full error chain
            eprintln!("Error: {}", e);
            
            // Iterate through causes
            let mut cause = e.source();
            while let Some(src) = cause {
                eprintln!("Caused by: {}", src);
                cause = src.source();
            }
            
            // Or use format for debugging
            eprintln!("Full chain: {:?}", e);
        }
        Ok(_) => {}
    }
    
    Ok(())
}

Context preserves the error chain accessible via .source().

Context with Custom Error Types

use anyhow::{Context, Result};
use std::fmt;
 
#[derive(Debug)]
struct ValidationError {
    field: String,
    message: String,
}
 
impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Validation error on {}: {}", self.field, self.message)
    }
}
 
impl std::error::Error for ValidationError {}
 
fn validate_input(input: &str) -> Result<()> {
    if input.is_empty() {
        return Err(ValidationError {
            field: "input".to_string(),
            message: "cannot be empty".to_string(),
        }.into());
    }
    Ok(())
}
 
fn main() -> Result<()> {
    validate_input("")
        .context("Input validation failed")?;
    Ok(())
}

.context() works with any error convertible to anyhow::Error.

with_context Closures Capture Environment

use anyhow::{Context, Result};
 
fn process_file(path: &str) -> Result<String> {
    let operation = "read";
    
    std::fs::read_to_string(path)
        .with_context(|| {
            // Closure captures environment
            format!("Operation '{}' failed for file: {}", operation, path)
        })
}
 
fn main() -> Result<()> {
    let content = process_file("nonexistent.txt")?;
    Ok(())
}

Closures can capture variables from the surrounding scope.

Multiple Context Layers

use anyhow::{Context, Result};
 
fn inner_operation() -> Result<i32> {
    Err(anyhow::anyhow!("disk full"))
}
 
fn middle_operation() -> Result<i32> {
    inner_operation()
        .context("Middle layer failed")
}
 
fn outer_operation() -> Result<i32> {
    middle_operation()
        .context("Outer layer failed")
}
 
fn main() {
    match outer_operation() {
        Err(e) => {
            eprintln!("Error: {:?}", e);
            // Shows:
            // Outer layer failed
            // Caused by: Middle layer failed
            // Caused by: disk full
        }
        Ok(_) => {}
    }
}

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

Context with Option Types

use anyhow::{Context, Result};
 
fn find_user(id: u32) -> Result<String> {
    let users = vec![
        (1, "Alice"),
        (2, "Bob"),
    ];
    
    users.into_iter()
        .find(|(user_id, _)| *user_id == id)
        .map(|(_, name)| name.to_string())
        .context(format!("User {} not found", id))
}
 
fn main() -> Result<()> {
    let user = find_user(3)?;
    println!("Found: {}", user);
    Ok(())
}

.context() converts Option to Result with context.

with_context for Option Types

use anyhow::{Context, Result};
 
fn find_config(key: &str) -> Result<String> {
    std::env::var(key)
        .ok()
        .with_context(|| format!("Environment variable {} not set", key))
}
 
fn main() -> Result<()> {
    let config = find_config("MY_CONFIG")?;
    println!("Config: {}", config);
    Ok(())
}

.with_context() also works on Option types.

Avoiding Allocation on Success Path

use anyhow::{Context, Result};
 
struct Config {
    path: String,
    version: u32,
}
 
fn load_config(path: &str) -> Result<Config> {
    // BAD: String allocation happens even on success
    let content = std::fs::read_to_string(path)
        .context(format!("Failed to load config from '{}'", path))?;
    
    // GOOD: String allocation only on error
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to load config from '{}'", path))?;
    
    // Parse config...
    Ok(Config { path: path.to_string(), version: 1 })
}

Use .with_context() when formatting is expensive.

Debug Format for Context

use anyhow::{Context, Result};
 
fn process_value(value: i32) -> Result<()> {
    if value < 0 {
        return Err(anyhow::anyhow!("Negative value"));
    }
    Ok(())
}
 
fn main() -> Result<()> {
    process_value(-1)
        .with_context(|| format!("Failed to process value: debug={:?}", -1))?;
    Ok(())
}

Context can include debug formatting for richer information.

Context on Result Methods

use anyhow::{Context, Result};
 
fn main() -> Result<()> {
    // context on Option
    let value: Option<i32> = None;
    let result = value.context("No value found");
    
    // with_context on Option
    let result2: Result<i32> = None::<i32>
        .with_context(|| "Value was None".to_string());
    
    // Both convert Option<T> to Result<T, anyhow::Error>
    
    Ok(())
}

Both methods work on Option to convert to Result.

Error Wrapping vs Context

use anyhow::{Context, Result};
 
fn main() -> Result<()> {
    // context: Adds context to existing error
    let err1: Result<i32> = Err(anyhow::anyhow!("base error"))
        .context("context message");
    
    // error!: Creates new error wrapping the old one
    let err2: Result<i32> = Err(anyhow::anyhow!("base error"))
        .map_err(|e| anyhow::anyhow!("wrapped: {}", e));
    
    // context is cleaner for adding information
    // map_err is for complete error transformation
    
    Ok(())
}

.context() is idiomatic for adding context; .map_err() for transformation.

Conditional Context

use anyhow::{Context, Result};
 
fn read_config_with_fallback(primary: &str, fallback: &str) -> Result<String> {
    std::fs::read_to_string(primary)
        .with_context(|| format!("Failed to read primary config: {}", primary))
        .or_else(|e| {
            eprintln!("Warning: {}", e);
            std::fs::read_to_string(fallback)
                .with_context(|| format!("Failed to read fallback config: {}", fallback))
        })
}
 
fn main() -> Result<()> {
    let config = read_config_with_fallback("config.toml", "config.default.toml")?;
    println!("Loaded config");
    Ok(())
}

.with_context() works with combinators like .or_else().

Real-World Usage Pattern

use anyhow::{Context, Result};
use std::fs::File;
use std::io::Read;
 
fn load_user_data(user_id: u32) -> Result<UserData> {
    let path = format!("/data/users/{}.json", user_id);
    
    let mut file = File::open(&path)
        .with_context(|| format!("Cannot open user file: {}", path))?;
    
    let mut content = String::new();
    file.read_to_string(&mut content)
        .with_context(|| format!("Cannot read user file: {}", path))?;
    
    let user: UserData = serde_json::from_str(&content)
        .with_context(|| {
            format!("Invalid user data format in: {}", path)
        })?;
    
    Ok(user)
}
 
#[derive(Debug)]
struct UserData {
    id: u32,
    name: String,
}
 
fn main() -> Result<()> {
    let user = load_user_data(42)?;
    println!("Loaded: {:?}", user);
    Ok(())
}

Use .with_context() at each fallible operation for detailed errors.

Comparison Table

Feature .context() .with_context()
Argument type impl Into<anyhow::Error> FnOnce() -> impl Into<anyhow::Error>
Evaluation Always (eager) Only on error (lazy)
Performance on success Always pays context cost No cost on success
Syntax .context(msg) .with_context(|| msg)
Use case Static strings Dynamic/format strings

Synthesis

The choice between .context() and .with_context() centers on when the context string is computed:

.context(msg) evaluates immediately, regardless of whether the Result is Ok or Err. Use this when:

  • The message is a static string (no formatting cost)
  • The message is already computed and available
  • Performance on the success path is not critical

.with_context(|| msg) evaluates the closure only when the Result is Err. Use this when:

  • The message requires format! or other string operations
  • Computing the message involves expensive operations
  • Performance on the success path matters

Key insight: The difference is purely about eager vs lazy evaluation. Both produce identical error chains when an error occurs. The .with_context() form is more idiomatic for any context that involves formatting, as it follows the principle of not paying for what you don't use—formatting only happens when the context is actually needed for error reporting. For static strings, both forms are equivalent, and .context() is slightly cleaner syntax.

Best practice: Default to .with_context(|| ...) for any dynamic context. Only use .context(...) for static strings or when the context is already owned. This keeps the success path fast while still providing detailed error context when failures occur.