How does anyhow::Context::context add contextual information to errors without wrapping types?
context adds contextual information by converting errors into anyhow::Error, which stores the original error as a cause and the context string as additional metadata, allowing the error to be displayed with its context without requiring a wrapper type to be manually defined. The Context trait is implemented for Result<T, E> where E: StdError + Send + Sync + 'static, enabling .context() to be called on any Result with a standard error type.
Basic Context Usage
use anyhow::{Context, Result};
use std::fs::File;
fn basic_context() -> Result<()> {
// Without context:
let file = File::open("config.toml")?;
// Error: No such file or directory (os error 2)
// With context:
let file = File::open("config.toml")
.context("failed to open configuration file")?;
// Error: failed to open configuration file
// Caused by:
// No such file or directory (os error 2)
Ok(())
}The context message is prepended to the error chain when displayed.
The Context Trait
use anyhow::Context;
// The Context trait is defined as:
// pub trait Context<T, E> {
// fn context<C>(self, context: C) -> Result<T, Error>
// where
// C: Display + Send + Sync + 'static;
//
// fn with_context<C, F>(self, f: F) -> Result<T, Error>
// where
// C: Display + Send + Sync + 'static,
// F: FnOnce() -> C;
// }
//
// It's implemented for Result<T, E> where E: StdError + Send + Sync + 'static
fn trait_explanation() -> anyhow::Result<()> {
// context() converts the error into anyhow::Error
// The context string is stored alongside the error
// Works with any error type implementing std::error::Error
std::fs::read_to_string("file.txt")
.context("failed to read file")?;
// The original error type is preserved inside anyhow::Error
// It can be retrieved with error.source() or downcast
Ok(())
}Context is implemented broadly for Result types, enabling context on any standard error.
How Errors Are Stored
use anyhow::{Context, Error};
fn error_storage() {
// When .context() is called:
// 1. The original error is wrapped in anyhow::Error
// 2. The context string is stored as "context" for display
// 3. The resulting type is Result<T, anyhow::Error>
// anyhow::Error is a thin wrapper around a boxed error trait object
// It stores:
// - The original error (as dyn StdError)
// - The context message (as dyn Display + Send + Sync)
// - Backtrace (if RUST_BACKTRACE is set)
let result: Result<String, std::io::Error> = std::fs::read_to_string("missing.txt");
let result_with_context = result.context("loading configuration");
// result_with_context is now Result<String, anyhow::Error>
// The std::io::Error is preserved inside
// The context "loading configuration" is attached
}anyhow::Error stores both the original error and context for later display.
Context vs With Context
use anyhow::{Context, Result};
fn context_vs_with_context() -> Result<()> {
// context() takes a static string or value
let _file = std::fs::File::open("config.toml")
.context("failed to open config file")?;
// with_context() takes a closure, evaluated only on error
let filename = "config.toml";
let _file = std::fs::File::open(filename)
.with_context(|| format!("failed to open file: {}", filename))?;
// with_context is useful for:
// 1. Expensive operations that should only run on error
// 2. Dynamic values that need to be captured
// 3. Computed context strings
// The closure is only called if the Result is Err
// This avoids allocation for successful paths
Ok(())
}with_context defers context computation until an error occurs.
Error Chain Display
use anyhow::{Context, Result};
fn error_chain() -> Result<()> {
std::fs::read_to_string("config.toml")
.context("reading configuration")?;
Ok(())
}
fn demonstrate_chain() {
match error_chain() {
Ok(()) => {}
Err(e) => {
// Display shows context + cause
println!("Error: {}", e);
// Error: reading configuration
// Caused by:
// No such file or directory (os error 2)
// Debug shows full chain
println!("Error: {:?}", e);
// Error: reading configuration
//
// Caused by:
// No such file or directory (os error 2)
// Chain traversal
let mut source = e.source();
while let Some(cause) = source {
println!("Caused by: {}", cause);
source = cause.source();
}
}
}
}Context appears in both Display and Debug output, with the original error as a cause.
Nested Context
use anyhow::{Context, Result};
fn nested_context() -> Result<()> {
std::fs::read_to_string("config.toml")
.context("loading config file")?
.parse::<toml::Value>()
.context("parsing config content")?;
Ok(())
}
// Multiple context layers create an error chain:
// Error: parsing config content
// Caused by:
// 0: loading config file
// 1: expected equals, found newline at line 5
// 2: No such file or directory (os error 2)
//
// Note: The chain includes all contexts and the original error
fn load_config() -> Result<()> {
let content = std::fs::read_to_string("config.toml")
.context("failed to read configuration file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse configuration")?;
validate_config(&config)
.context("invalid configuration")?;
Ok(())
}Each .context() call adds a layer to the error chain.
No Wrapping Type Required
use anyhow::{Context, Result};
// Traditional error handling with custom types:
mod traditional {
use std::fmt;
#[derive(Debug)]
pub enum ConfigError {
Io(std::io::Error),
Parse(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "IO error: {}", e),
ConfigError::Parse(s) => write!(f, "Parse error: {}", s),
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(e) => Some(e),
ConfigError::Parse(_) => None,
}
}
}
// This requires defining wrapper types for every context
}
// With anyhow, no wrapper needed:
fn with_anyhow() -> Result<()> {
std::fs::read_to_string("config.toml")
.context("reading configuration")?;
Ok(())
}
// The context is added dynamically
// No enum variants or Display impl neededcontext avoids boilerplate wrapper types while providing rich error information.
Working with Error
use anyhow::{Context, Error, Result};
fn working_with_error() -> Result<()> {
let result: Result<()> = Err(Error::msg("something failed"));
// Error::msg creates an error from a message
let err = Error::msg("custom error message");
// context works on Result<_, Error> too
let result: Result<()> = Err(err)
.context("additional context");
// But context primarily converts non-anyhow errors
// to anyhow::Error with attached context
Ok(())
}
fn downcasting() {
let result: Result<String, std::io::Error> =
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "file missing"));
let err = result.context("loading data").unwrap_err();
// Downcast to original error type
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
println!("IO error kind: {:?}", io_err.kind());
}
// The original error is preserved inside anyhow::Error
}anyhow::Error preserves the original error for downcasting.
Context in Libraries vs Applications
use anyhow::{Context, Result};
// Libraries should use Result<T, Box<dyn StdError>> or similar
// And return concrete error types
// Library code:
mod library {
use std::io;
pub fn load_config(path: &str) -> Result<String, io::Error> {
std::fs::read_to_string(path)
}
}
// Application code:
fn application() -> Result<()> {
// Applications use anyhow and .context() to add context
let config = library::load_config("config.toml")
.context("failed to load application configuration")?;
// Libraries return specific errors
// Applications add context with anyhow
Ok(())
}
// Guideline:
// - Libraries: return Result<T, ConcreteError>
// - Applications: use anyhow for error handling and .context()Use anyhow in applications; libraries should return specific error types.
Performance Considerations
use anyhow::{Context, Result};
fn performance() -> Result<()> {
// context() has minimal overhead on success
// The string is stored in the Result type
// with_context() is more efficient for expensive context:
let _file = std::fs::File::open("config.toml")
.with_context(|| {
// This closure only runs on error
// Expensive operation here won't affect success path
format!("failed to open: {:?}", std::env::current_dir())
})?;
// For hot paths, with_context avoids:
// - String allocation on success
// - Computation of context string
// context() with static strings is very cheap
let _file = std::fs::File::open("data.txt")
.context("opening data file")?; // Static string, minimal cost
Ok(())
}with_context defers expensive context computation until an error occurs.
Combining Multiple Error Types
use anyhow::{Context, Result};
use std::num::ParseIntError;
fn combine_errors() -> Result<()> {
// Different error types unified through context
let content = std::fs::read_to_string("config.txt")
.context("reading file")?; // io::Error -> anyhow::Error
let number: i32 = content.trim().parse()
.context("parsing number")?; // ParseIntError -> anyhow::Error
let addr: std::net::SocketAddr = "invalid".parse()
.context("parsing address")?; // AddrParseError -> anyhow::Error
// All different error types converted to anyhow::Error
// With context attached to each
Ok(())
}
// This unifies error types without defining a custom enumcontext converts any std::error::Error into anyhow::Error with attached context.
Error Sources and Chains
use anyhow::{Context, Result};
fn chain_example() -> Result<()> {
std::fs::read_to_string("missing.txt")
.context("step 1: reading file")?
.parse::<i32>()
.context("step 2: parsing number")?;
Ok(())
}
fn examine_chain() {
if let Err(e) = chain_example() {
// e is anyhow::Error
// The chain of causes
let mut current: Option<&dyn std::error::Error> = Some(&e);
let mut depth = 0;
while let Some(err) = current {
println!("{}: {}", depth, err);
current = err.source();
depth += 1;
}
// Note: anyhow's Error doesn't use the standard source chain
// exactly the same way. The context is stored separately.
// Use error chain iterator:
for cause in e.chain() {
println!("Caused by: {}", cause);
}
}
}The chain() method provides access to all error causes.
Backtraces
use anyhow::{Context, Result};
fn backtraces() -> Result<()> {
// Backtraces are captured automatically if RUST_BACKTRACE=1
std::fs::read_to_string("missing.txt")
.context("loading file")?;
// The backtrace shows where .context() was called
// This helps identify where the error originated
Ok(())
}
// Backtrace is stored inside anyhow::Error
// Access with error.backtrace()
fn print_backtrace() {
if let Err(e) = backtraces() {
if let Some(bt) = e.backtrace() {
println!("Backtrace:\n{}", bt);
}
}
}Backtraces are captured when RUST_BACKTRACE is enabled.
Common Patterns
use anyhow::{Context, Result};
fn common_patterns() -> Result<()> {
// Pattern 1: Context at function boundaries
fn read_config(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.context(format!("loading config from {}", path))
}
// Pattern 2: Multiple contexts for different operations
let data = std::fs::read_to_string("input.txt")
.context("reading input file")?;
let parsed: Vec<i32> = data.lines()
.map(|line| line.parse())
.collect::<Result<Vec<_>, _>>()
.context("parsing input data")?;
// Pattern 3: Context with dynamic values
let filename = "config.json";
let content = std::fs::read_to_string(filename)
.with_context(|| format!("reading file: {}", filename))?;
// Pattern 4: Early return with context
fn find_user(id: u64) -> Result<User> {
database::find_user(id)
.with_context(|| format!("user {} not found", id))
}
Ok(())
}
struct User;
mod database {
use super::User;
use anyhow::Result;
pub fn find_user(_id: u64) -> Result<User> {
anyhow::bail!("not found")
}
}Common patterns use context at function boundaries and with dynamic values.
Synthesis
Quick reference:
| Method | Signature | When to Use |
|---|---|---|
.context() |
context<C>(msg: C) -> Result<T, Error> |
Static strings, simple context |
.with_context() |
with_context<C, F>(f: F) -> Result<T, Error> |
Dynamic context, expensive computation |
How context works internally:
// Simplified mental model of anyhow::Error:
//
// struct Error {
// inner: Box<ErrorImpl>,
// }
//
// struct ErrorImpl {
// source: Option<Box<dyn StdError + Send + Sync>>,
// message: Option<String>, // or context
// backtrace: Option<Backtrace>,
// }
//
// When .context() is called:
// 1. The original error is boxed as source
// 2. The context string becomes the message
// 3. The error chain is: context -> sourceKey insight: anyhow::Context::context adds contextual information without requiring wrapper types by converting errors into anyhow::Error, which stores both the original error and the context string internally. The Context trait is implemented for Result<T, E> where E: StdError + Send + Sync + 'static, enabling .context() calls on virtually any Result. When the error is displayed, the context appears first, followed by the original error as a cause. This approach provides the ergonomics of custom error wrapper types without requiring boilerplate enum definitions. The .with_context() variant takes a closure that's only evaluated on error, avoiding expensive string formatting on success paths. Context stacks: multiple .context() calls create a chain of context messages, all preserved for debugging. Use context in application code to enrich errors with location and operation information; libraries should return specific error types and let applications add context. The original error remains accessible via downcast_ref::<T>() for type-specific handling, and the full chain is iterable via error.chain(). Backtraces are automatically captured when RUST_BACKTRACE is set, showing where .context() was called.
