Loading page…
Rust walkthroughs
Loading page…
anyhow::Context add contextual information to errors without manual error type definitions?The anyhow::Context trait adds descriptive context to errors by wrapping them with additional information—file paths, operation names, user messages—without requiring custom error type definitions. When applied via the .context() method on Result, it creates an anyhow::Error that chains the original error with the provided context string. This enables rich error messages that trace the full path of failure: "failed to open config file: permission denied" rather than just "permission denied". The approach eliminates boilerplate error types for applications that need informative errors but don't require type-based error handling.
use anyhow::{Context, Result};
use std::fs;
fn read_config() -> Result<String> {
let content = fs::read_to_string("config.toml")
.context("failed to read config file")?;
Ok(content)
}The .context() method wraps any error with a descriptive message.
use anyhow::{Context, Result};
use std::fs;
fn load_config() -> Result<()> {
let content = fs::read_to_string("config.toml")
.context("failed to open config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse config")?;
Ok(())
}
fn main() {
match load_config() {
Ok(_) => println!("Config loaded"),
Err(e) => {
// Error chain: "failed to parse config"
// "failed to open config file"
// "No such file or directory (os error 2)"
println!("Error: {:?}", e);
// Print full chain:
for cause in e.chain() {
println!(" Caused by: {}", cause);
}
}
}
}
struct Config {}Each .context() call adds a link to the error chain.
use anyhow::{Context, Result};
use std::fs;
// Without anyhow: define custom error types
mod traditional {
use std::fmt;
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(String),
ConfigError(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "IO error: {}", e),
AppError::ParseError(s) => write!(f, "Parse error: {}", s),
AppError::ConfigError(s) => write!(f, "Config error: {}", s),
}
}
}
impl std::error::Error for AppError {}
// Plus From implementations...
}
// With anyhow: just add context
fn read_data() -> Result<String> {
let data = std::fs::read_to_string("data.txt")
.context("failed to read data file")?;
Ok(data)
}Context eliminates the need for boilerplate error type definitions.
use anyhow::{Context, Result};
use std::fs;
fn read_user_file(username: &str) -> Result<String> {
let path = format!("users/{}.json", username);
let content = fs::read_to_string(&path)
.context(format!("failed to read file for user '{}'", username))?;
Ok(content)
}
fn main() -> Result<()> {
let content = read_user_file("alice")?;
Ok(())
}Context strings can be formatted with dynamic values.
use anyhow::{Context, Result};
use std::fs;
fn process_file(path: &str) -> Result<String> {
// with_context takes a closure - only evaluated on error
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read file: {}", path))?;
// String formatting only happens if read_to_string fails
Ok(content)
}.with_context() defers string allocation until an error occurs.
use anyhow::{Context, Result};
fn comparison() -> Result<()> {
// context(): Eager evaluation
// String allocated even if operation succeeds
std::fs::read_to_string("config.txt")
.context(format!("Failed to read {}", "config.txt"))?;
// with_context(): Lazy evaluation
// Closure only called if operation fails
std::fs::read_to_string("config.txt")
.with_context(|| format!("Failed to read {}", "config.txt"))?;
Ok(())
}with_context avoids string allocation on success.
use anyhow::{Context, Result};
fn deep_operation() -> Result<()> {
std::fs::read_to_string("config.toml")
.context("reading config")?;
Ok(())
}
fn middle_operation() -> Result<()> {
deep_operation()
.context("initializing application")?;
Ok(())
}
fn top_level() -> Result<()> {
middle_operation()
.context("starting server")?;
Ok(())
}
fn main() {
if let Err(e) = top_level() {
// Print the full error chain
println!("Error: {}", e);
println!("\nCauses:");
for cause in e.chain() {
println!(" - {}", cause);
}
// Or use Debug for more detail
println!("\nDebug: {:?}", e);
}
}The error chain preserves all context from bottom to top.
use anyhow::{Context, Result};
fn load_config() -> Result<Config> {
let content = std::fs::read_to_string("config.toml")
.context("failed to open config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse TOML")?;
validate_config(&config)
.context("config validation failed")?;
Ok(config)
}
fn validate_config(config: &Config) -> Result<()> {
if config.port == 0 {
return Err(anyhow::anyhow!("port cannot be 0"));
}
Ok(())
}
struct Config {
port: u16,
}Each layer adds specific context for debugging.
use anyhow::{Context, Result};
use std::fs;
fn mixed_errors() -> Result<()> {
// IO error
let content = fs::read_to_string("data.txt")
.context("reading data file")?;
// Parse error (from serde_json)
let data: serde_json::Value = serde_json::from_str(&content)
.context("parsing JSON data")?;
// Custom error
if data["enabled"].as_bool().unwrap_or(false) {
return Err(anyhow::anyhow!("feature not enabled"));
}
Ok(())
}Context works uniformly with any error type implementing std::error::Error.
use anyhow::{Context, Result};
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("failed to load config from 'config.toml'")?
// Note: context is added only on error, not on success
// The ? operator propagates the error with context
}
fn parse_config(content: &str) -> Result<Config> {
toml::from_str(content)
.context("config is not valid TOML")?
// Again, context added only on parse failure
}
struct Config {}Context attaches to errors during propagation.
use anyhow::{Context, Result};
fn main() -> Result<()> {
run().context("application failed")?;
Ok(())
}
fn run() -> Result<()> {
std::fs::read_to_string("missing.txt")
.context("reading configuration")?;
Ok(())
}
// On error, different display formats:
//
// println!("{}", e);
// Output: reading configuration
//
// println!("{:?}", e);
// Output: Error { context: "reading configuration", source: Os { code: 2, kind: NotFound, message: "No such file or directory" } }
//
// println!("{:#?}", e);
// Output: Formatted multi-line debug viewDifferent display formats show varying levels of detail.
use anyhow::{Context, Result};
// Library function with detailed context
pub fn parse_database_url(url: &str) -> Result<DatabaseConfig> {
let parsed = url::Url::parse(url)
.context("invalid database URL format")?;
let host = parsed.host_str()
.context("database URL missing host")?
.to_string();
let port = parsed.port()
.context("database URL missing port")?;
Ok(DatabaseConfig { host, port })
}
pub fn connect_database(config: &DatabaseConfig) -> Result<Connection> {
// Library provides context for failures
let conn = establish_connection(&config.host, config.port)
.context(format!("failed to connect to {}:{}", config.host, config.port))?;
Ok(conn)
}
struct DatabaseConfig {
host: String,
port: u16,
}
struct Connection;
fn establish_connection(_host: &str, _port: u16) -> Result<Connection> {
Ok(Connection)
}
mod url {
pub struct Url;
impl Url {
pub fn parse(_s: &str) -> Result<Self> { Ok(Self) }
pub fn host_str(&self) -> Option<&str> { Some("localhost") }
pub fn port(&self) -> Option<u16> { Some(5432) }
}
}Libraries add context at each failure point.
use anyhow::{Context, Result};
// Application code: user-focused context
fn load_user_config() -> Result<Config> {
std::fs::read_to_string("~/.config/app.toml")
.context("Could not load user configuration. \
Please check that ~/.config/app.toml exists.")?;
// Application context is user-facing
Ok(Config {})
}
// Library code: developer-focused context
pub fn parse_config(content: &str) -> Result<Config> {
toml::from_str(content)
.context("config parsing failed")?;
// Library context is for developers
Ok(Config {})
}
struct Config {}Application context can be user-facing; library context targets developers.
use anyhow::{Context, Result};
fn main() -> Result<()> {
if let Err(e) = run_application() {
// Log full error chain
eprintln!("Application error: {}", e);
// For debugging
eprintln!("\nBacktrace:\n{:?}", e);
// For users (show root cause)
if let Some(cause) = e.chain().last() {
eprintln!("\nRoot cause: {}", cause);
}
std::process::exit(1);
}
Ok(())
}
fn run_application() -> Result<()> {
std::fs::read_to_string("config.toml")
.context("loading configuration")?;
Ok(())
}Error chains can be displayed at different levels of detail.
use anyhow::{Context, Result};
fn with_backtrace() -> Result<()> {
// Enable backtrace capture (requires RUST_BACKTRACE=1)
std::fs::read_to_string("missing.txt")
.context("reading configuration")?;
Ok(())
}
fn main() -> Result<()> {
// Set RUST_BACKTRACE=1 environment variable
// to capture backtraces in anyhow::Error
if let Err(e) = with_backtrace() {
// Backtrace is included if available
println!("Error: {:?}", e);
}
Ok(())
}anyhow::Error captures backtraces when RUST_BACKTRACE is enabled.
use anyhow::{Context, Result};
fn validate_user(username: &str, email: &str, age: u32) -> Result<User> {
let username = validate_username(username)
.context("invalid username")?;
let email = validate_email(email)
.context("invalid email")?;
let age = validate_age(age)
.context("invalid age")?;
Ok(User { username, email, age })
}
fn validate_username(s: &str) -> Result<String> {
if s.is_empty() {
return Err(anyhow::anyhow!("username cannot be empty"));
}
if s.len() > 20 {
return Err(anyhow::anyhow!("username too long (max 20 characters)"));
}
Ok(s.to_string())
}
fn validate_email(s: &str) -> Result<String> {
if !s.contains('@') {
return Err(anyhow::anyhow!("email must contain @"));
}
Ok(s.to_string())
}
fn validate_age(age: u32) -> Result<u32> {
if age < 13 {
return Err(anyhow::anyhow!("must be at least 13 years old"));
}
if age > 150 {
return Err(anyhow::anyhow!("age seems unrealistic"));
}
Ok(age)
}
struct User {
username: String,
email: String,
age: u32,
}Context wraps validation errors with field-specific context.
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
struct FileProcessor;
impl FileProcessor {
pub fn process(input: &Path, output: &Path) -> Result<()> {
let content = fs::read_to_string(input)
.with_context(|| format!("failed to read input file: {:?}", input))?;
let processed = Self::transform(&content)
.context("transformation failed")?;
fs::write(output, processed)
.with_context(|| format!("failed to write output file: {:?}", output))?;
Ok(())
}
fn transform(content: &str) -> Result<String> {
if content.is_empty() {
return Err(anyhow::anyhow!("content is empty"));
}
Ok(content.to_uppercase())
}
}File operations add context with file paths for debugging.
use anyhow::{Context, Result};
struct ApiClient {
base_url: String,
}
impl ApiClient {
async fn fetch_user(&self, id: u64) -> Result<User> {
let url = format!("{}/users/{}", self.base_url, id);
let response = reqwest::get(&url)
.await
.with_context(|| format!("failed to connect to {}", url))?;
let status = response.status();
if !status.is_success() {
return Err(anyhow::anyhow!("API returned status {}", status));
}
let user = response.json::<User>()
.await
.context("failed to parse user response")?;
Ok(user)
}
}
struct User {
id: u64,
name: String,
}
mod reqwest {
pub async fn get(_url: &str) -> Result<Response> { Ok(Response) }
pub struct Response;
impl Response {
pub fn status(&self) -> Status { Status(200) }
pub async fn json<T>(&self) -> Result<T> { todo!() }
}
pub struct Status(u16);
impl Status {
pub fn is_success(&self) -> bool { true }
}
}Network operations add context with URLs and status codes.
use anyhow::{Context, Result};
use std::path::Path;
struct AppConfig {
database: DatabaseConfig,
server: ServerConfig,
}
struct DatabaseConfig {
url: String,
pool_size: u32,
}
struct ServerConfig {
host: String,
port: u16,
}
impl AppConfig {
pub fn load(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {:?}", path))?;
let raw: toml::Value = toml::from_str(&content)
.context("config is not valid TOML")?;
let database = Self::parse_database(&raw)
.context("invalid database configuration")?;
let server = Self::parse_server(&raw)
.context("invalid server configuration")?;
Ok(Self { database, server })
}
fn parse_database(raw: &toml::Value) -> Result<DatabaseConfig> {
let db = raw.get("database")
.context("missing [database] section")?;
let url = db.get("url")
.and_then(|v| v.as_str())
.context("database.url is required")?
.to_string();
let pool_size = db.get("pool_size")
.and_then(|v| v.as_integer())
.unwrap_or(10) as u32;
Ok(DatabaseConfig { url, pool_size })
}
fn parse_server(raw: &toml::Value) -> Result<ServerConfig> {
let server = raw.get("server")
.context("missing [server] section")?;
let host = server.get("host")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0.0")
.to_string();
let port = server.get("port")
.and_then(|v| v.as_integer())
.unwrap_or(8080) as u16;
Ok(ServerConfig { host, port })
}
}Configuration loading uses context at each parsing step.
Context methods:
| Method | Evaluation | Use Case |
|--------|------------|----------|
| .context(msg) | Eager | Simple static messages |
| .with_context(\|\| msg) | Lazy | Formatted messages with dynamic data |
Error chain structure:
Top-level error
└── Middle context: "initializing application"
└── Inner context: "reading config"
└── Root cause: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Comparison with custom error types:
| Approach | Boilerplate | Flexibility | Type Safety |
|----------|-------------|-------------|-------------|
| anyhow::Context | Minimal | High | Low (runtime) |
| Custom error types | High | Medium | High (compile-time) |
| thiserror derive | Medium | Medium | High (compile-time) |
When to use Context:
| Use Case | Context Approach | |----------|-----------------| | Application code | Excellent fit | | Prototyping | Excellent fit | | Library with simple errors | Good fit | | Library needing type-based error handling | Use custom types | | Matching on specific errors | Use custom types |
Key insight: anyhow::Context enables rich error messages without the boilerplate of custom error types by wrapping any error with contextual strings. Each .context() call adds a link to an error chain that can be traversed for debugging: the top-level message describes what operation failed, intermediate messages add context about which subsystem or step, and the root cause shows the underlying error. This is particularly valuable in application code where error messages should guide users and developers toward fixes, but where type-based error matching isn't needed. The with_context variant defers string formatting until an error occurs, avoiding allocation on success. For applications that need informative errors without matching on specific error types, Context provides maximum utility with minimal code.