Loading pageā¦
Rust walkthroughs
Loading pageā¦
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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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().
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.
| 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 |
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:
.with_context(|| msg) evaluates the closure only when the Result is Err. Use this when:
format! or other string operationsKey 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.