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.
