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.
