What is the difference between anyhow::Error::context and with_context for adding contextual information?
anyhow::Error::context and with_context both add contextual information to errors, but they differ in when the context is evaluated: context eagerly evaluates its argument immediately, while with_context lazily evaluates its closure only when an error actually occurs. This distinction matters for performance and side effects when adding context to operations that frequently succeed.
Understanding Error Context in anyhow
use anyhow::{Context, Result};
fn understanding_context() {
// anyhow allows adding context to errors for better error messages
// Both context() and with_context() attach explanatory information
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to read configuration file")?;
Ok(String::new())
}
// When this fails, the error includes both:
// - The underlying IO error
// - The context: "Failed to read configuration file"
// Error message: "Failed to read configuration file"
// "Caused by:"
// " No such file or directory (os error 2)"
}Context adds explanatory information to error messages, creating a chain of causes.
The context Method
use anyhow::{Context, Result};
fn context_eager_evaluation() -> Result<()> {
// context() takes a value that implements Display
// The context string is created immediately, regardless of success/failure
fn expensive_context() -> String {
println!("Creating context string..."); // This always runs!
"Failed to process data".to_string()
}
// Even if this succeeds, expensive_context() is called
let result: Result<()> = Ok(())
.context(expensive_context());
// "Creating context string..." was printed even though no error occurred
// context() signature:
// fn context<C>(self, context: C) -> Result<T>
// where C: Display + Send + Sync + 'static
// The context is eagerly evaluated
Ok(())
}context eagerly evaluates its argument, even when the operation succeeds.
The with_context Method
use anyhow::{Context, Result};
fn with_context_lazy_evaluation() -> Result<()> {
// with_context() takes a closure that returns context
// The closure is only called if there's an error
fn expensive_context() -> String {
println!("Creating context string..."); // Only runs on error!
"Failed to process data".to_string()
}
// The closure is NOT called if this succeeds
let result: Result<()> = Ok(())
.with_context(|| expensive_context());
// "Creating context string..." was NOT printed
// with_context() signature:
// fn with_context<C, F>(self, f: F) -> Result<T>
// where F: FnOnce() -> C, C: Display + Send + Sync + 'static
// The closure is lazily evaluated - only on error
// Now with an actual error:
let result: Result<()> = Err(anyhow::anyhow!("Something failed"))
.with_context(|| expensive_context());
// "Creating context string..." IS printed because there was an error
Ok(())
}with_context lazily evaluates its closure only when an error occurs.
Performance Impact
use anyhow::{Context, Result};
fn performance_comparison() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Method β Evaluation β Success Case β Error Case β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β context β Eager β Allocates β Allocates β
// β with_context β Lazy β No allocationβ Allocates β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Scenario: Operation succeeds 99% of the time
// Using context - creates string every time:
fn process_item_context(item: &str) -> Result<()> {
// String allocation happens on every call
parse_item(item).context(format!("Failed to parse item: {}", item))?;
Ok(())
}
// Using with_context - only creates string on error:
fn process_item_with_context(item: &str) -> Result<()> {
// String allocation only happens on error (1% of the time)
parse_item(item).with_context(|| format!("Failed to parse item: {}", item))?;
Ok(())
}
// For high-frequency operations with low error rates,
// with_context avoids thousands of unnecessary allocations
fn parse_item(_item: &str) -> Result<()> {
Ok(())
}
}with_context avoids unnecessary allocations when operations succeed.
String Formatting Costs
use anyhow::{Context, Result};
fn string_formatting_costs() {
// Expensive string operations are where with_context shines
fn format_expensive_context(id: u64, data: &str) -> String {
// Imagine this does complex work:
// - Database lookup
// - Heavy computation
// - Complex formatting
format!("Operation failed for ID {} with data: {}", id, data)
}
// With context - expensive format every call:
fn process_context(id: u64, data: &str) -> Result<()> {
let _ = Ok::<(), anyhow::Error>(())
.context(format_expensive_context(id, data));
// format_expensive_context runs even on success!
Ok(())
}
// With with_context - expensive format only on error:
fn process_with_context(id: u64, data: &str) -> Result<()> {
let _ = Ok::<(), anyhow::Error>(())
.with_context(|| format_expensive_context(id, data));
// format_expensive_context only runs if there's an error
Ok(())
}
// If success rate is 99.9%, with_context avoids 999 unnecessary calls
}Expensive formatting in context strings should use with_context.
Debug and Display Context
use anyhow::{Context, Result};
fn debug_vs_display() {
// context() accepts anything implementing Display
// with_context() closure returns something implementing Display
// Using string literals - no performance difference
fn read_file() -> Result<String> {
std::fs::read_to_string("data.txt")
.context("Failed to read data file") // String literal, cheap
}
// Both work the same for simple strings
fn read_file_lazy() -> Result<String> {
std::fs::read_to_string("data.txt")
.with_context(|| "Failed to read data file") // Same effect
}
// For simple static strings, context() is fine:
// - No allocation for &'static str
// - No closure overhead
// For dynamic strings, consider with_context:
fn process_dynamic(filename: &str) -> Result<String> {
std::fs::read_to_string(filename)
.with_context(|| format!("Failed to read file: {}", filename))
}
}Static strings work fine with context; dynamic strings benefit from with_context.
Chaining Context
use anyhow::{Context, Result};
fn chaining_context() -> Result<()> {
// Both methods can be chained to build error context chains
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to read config file")
.context("Application initialization failed")?;
Ok(String::new())
}
// Error output shows the chain:
// Application initialization failed
// Caused by:
// Failed to read config file
// Caused by:
// No such file or directory (os error 2)
fn read_config_lazy() -> Result<String> {
std::fs::read_to_string("config.toml")
.with_context(|| "Failed to read config file")
.with_context(|| "Application initialization failed")?;
Ok(String::new())
}
// Mix and match based on needs:
fn read_config_mixed() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to read config file") // Static, eager is fine
.with_context(|| format!("Config path: {}", "config.toml"))?; // Dynamic, lazy
Ok(String::new())
}
Ok(())
}Both methods can be chained; mixing them based on context complexity is fine.
Side Effects in Context
use anyhow::{Context, Result};
use std::time::Instant;
fn side_effects_in_context() {
// Side effects in context are problematic with context()
fn log_context_creation(msg: &str) -> String {
println!("Creating context: {}", msg); // Side effect!
msg.to_string()
}
// Using context - side effect runs every time:
fn process_context() -> Result<()> {
Ok(())
.context(log_context_creation("This always runs"))?;
// "Creating context: This always runs" is printed
Ok(())
}
// Using with_context - side effect only on error:
fn process_with_context() -> Result<()> {
Ok(())
.with_context(|| log_context_creation("This only runs on error"))?;
// Nothing printed - no error occurred
Ok(())
}
// Common mistake: logging in context()
fn process_with_logging() -> Result<()> {
let start = Instant::now();
Ok(())
.context(format!("Operation took {:?}", start.elapsed()))?;
// Timer computation happens even on success!
Ok(())
}
// Better: use with_context for computed context
fn process_with_logging_better() -> Result<()> {
let start = Instant::now();
Ok(())
.with_context(|| format!("Operation took {:?}", start.elapsed()))?;
// Timer only computed if error occurs
Ok(())
}
}Side effects in context always run; with_context avoids them on success.
Use Cases for context
use anyhow::{Context, Result};
fn when_to_use_context() {
// Use context() when:
// 1. Static string context
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to read configuration")
}
// 2. Simple context where allocation cost is negligible
fn parse_number(s: &str) -> Result<i32> {
s.parse()
.context(format!("Failed to parse '{}' as number", s))
// Simple format, low frequency call
}
// 3. When you want the context computed regardless
fn critical_operation() -> Result<()> {
perform_critical()
.context("Critical operation failed - see logs")
// Context helps even in debugging successful flow
}
// 4. When the operation is infrequent
fn one_time_setup() -> Result<()> {
initialize()
.context("Setup failed")
// Called once, no performance concern
}
fn perform_critical() -> Result<()> { Ok(()) }
fn initialize() -> Result<()> { Ok(()) }
}Use context for static strings or low-frequency operations.
Use Cases for with_context
use anyhow::{Context, Result};
fn when_to_use_with_context() {
// Use with_context() when:
// 1. Dynamic context with formatting
fn read_user_file(user_id: u64) -> Result<String> {
std::fs::read_to_string(format!("users/{}.json", user_id))
.with_context(|| format!("Failed to read file for user {}", user_id))
}
// 2. Expensive context computation
fn process_large_data(data: &[u8]) -> Result<()> {
parse_data(data)
.with_context(|| {
// This hash is expensive - only compute on error
let hash = compute_hash(data);
format!("Data validation failed (hash: {})", hash)
})
}
// 3. High-frequency operations with low error rate
fn process_request(req: &Request) -> Result<()> {
validate_request(req)
.with_context(|| format!("Invalid request from {}", req.source))?;
handle_request(req)
.with_context(|| format!("Handler failed for {}", req.id))
}
// 4. Context that requires computation
fn save_state(state: &State) -> Result<()> {
write_state(state)
.with_context(|| {
let size = state.size_bytes();
format!("Failed to save {} bytes of state", size)
})
}
fn parse_data(_data: &[u8]) -> Result<()> { Ok(()) }
fn compute_hash(data: &[u8]) -> String { format!("{:x}", data.len()) }
fn validate_request(_req: &Request) -> Result<()> { Ok(()) }
fn handle_request(_req: &Request) -> Result<()> { Ok(()) }
fn write_state(_state: &State) -> Result<()> { Ok(()) }
}
struct Request { id: u64, source: String }
struct State;
impl State { fn size_bytes(&self) -> usize { 0 } }Use with_context for dynamic strings, expensive computation, or high-frequency operations.
Error Messages Comparison
use anyhow::{Context, Result};
fn error_messages() {
// Both produce identical error message format
fn with_context_method() -> Result<()> {
Err::<(), _>(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
))
.with_context(|| "Reading configuration failed")
}
fn with_context_method2() -> Result<()> {
Err::<(), _>(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
))
.context("Reading configuration failed")
}
// Both produce:
// Reading configuration failed
// Caused by:
// file not found
// The difference is ONLY in when the context string is created
}Both methods produce identical error messages; the difference is only evaluation timing.
Complete Summary
use anyhow::{Context, Result};
fn complete_summary() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β context() β with_context() β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β Evaluation β Eager (immediate) β Lazy (only on error) β
// β Argument type β impl Display β FnOnce() -> impl Display β
// β Success case β Context created β Context NOT created β
// β Error case β Context created β Context created β
// β Performance β Always pays cost β Cost only on error β
// β Side effects β Always runs β Only runs on error β
// β Use case β Static strings β Dynamic strings β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Decision guide:
// - Static string like "Failed to read"? β context("...")
// - Dynamic string like format!(...)? β with_context(|| format!(...))
// - Expensive computation in context? β with_context(|| ...)
// - High-frequency operation? β with_context(|| ...)
// - Side effects in context creation? β with_context(|| ...)
// - Low-frequency, simple context? β context("...")
// The resulting error messages are identical.
// The difference is purely about when the context string is created.
// Example showing the key difference:
let success: Result<()> = Ok(());
let failure: Result<()> = Err(anyhow::anyhow!("error"));
// With context - format runs regardless:
let _ = success.context(format!("Status: {}", compute_status()));
// compute_status() runs
// With with_context - format only runs on error:
let _ = success.with_context(|| format!("Status: {}", compute_status()));
// compute_status() does NOT run
let _ = failure.with_context(|| format!("Status: {}", compute_status()));
// compute_status() runs (error occurred)
fn compute_status() -> i32 { 42 }
}
// Key insight:
// Both context() and with_context() add the same contextual information
// to errors. The only difference is evaluation timing. context() eagerly
// creates the context string immediately, even when the operation
// succeeds. with_context() takes a closure that's only called when
// there's actually an error to add context to.
//
// Use context() for static strings or when you don't care about the
// allocation cost. Use with_context() when the context requires
// dynamic formatting, expensive computation, or side effects that
// shouldn't run on successful operations. The performance difference
// matters most for high-frequency operations with low error rates,
// where with_context() avoids thousands of unnecessary allocations.Key insight: context and with_context produce identical error messages but differ in evaluation timing. context eagerly evaluates its argument immediately, while with_context lazily evaluates its closure only when an error occurs. For static strings or low-frequency operations, context is simple and clear. For dynamic strings with format!, expensive computations, side effects, or high-frequency operations with low error rates, with_context avoids unnecessary work on successful operations. The choice between them is purely about performance and side effectsβboth produce the same error message format.
