Maintaining error chains and context
When errors occur deep in the call stack, you need context to understand:
Error: File not found
With context:
Error: Failed to load user configuration
Caused by: Failed to read config file
Caused by: File not found: /etc/app/config.toml
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct AppError {
context: String,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl AppError {
pub fn new(context: impl Into<String>) -> Self {
Self {
context: context.into(),
source: None,
}
}
pub fn with_source(context: impl Into<String>, source: impl Error + Send + Sync + 'static) -> Self {
Self {
context: context.into(),
source: Some(Box::new(source)),
}
}
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.context)
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|e| e.as_ref() as &(dyn Error + 'static))
}
}
// Usage
fn load_user_data(user_id: u64) -> Result<User, AppError> {
let path = format!("/data/users/{}.json", user_id);
std::fs::read_to_string(&path)
.map_err(|e| AppError::with_source(
format!("Failed to read user file: {}", path),
e
))?;
// ...
Ok(user)
}
use anyhow::{Context, Result};
fn process_order(order_id: u64) -> Result<()> {
let order_file = format!("orders/{}.json", order_id);
let contents = std::fs::read_to_string(&order_file)
.with_context(|| format!("Failed to read order file: {}", order_file))?;
let order: Order = serde_json::from_str(&contents)
.context("Failed to parse order JSON")?;
validate_order(&order)
.with_context(|| format!("Order {} validation failed", order_id))?;
process_payment(&order)
.context("Payment processing failed")?;
Ok(())
}
// Error output:
// Error: Payment processing failed
// Caused by: Failed to charge card
// Caused by: Network timeout
use anyhow::{Context, Result};
async fn fetch_user_with_posts(user_id: i64) -> Result<UserWithPosts> {
// Context for each operation
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(user_id)
.fetch_one(&pool)
.await
.with_context(|| format!("Failed to fetch user {}", user_id))?;
let posts = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE user_id = $1")
.bind(user_id)
.fetch_all(&pool)
.await
.with_context(|| format!("Failed to fetch posts for user {}", user_id))?;
// Aggregate
Ok(UserWithPosts { user, posts })
}
// Error chain example:
// Error: Failed to fetch posts for user 123
// Caused by: Database query failed
// Caused by: Connection timeout
use anyhow::{Context, Result, bail};
fn load_and_validate_config() -> Result<Config> {
// Try multiple locations
let config_content = try_load_from_env()
.or_else(|_| try_load_from_file("./config.toml"))
.or_else(|_| try_load_from_file("/etc/app/config.toml"))
.context("Failed to load config from any location")?;
let mut config: Config = toml::from_str(&config_content)
.context("Failed to parse TOML config")?;
// Validate with context
if config.port == 0 {
bail!("Invalid port number: 0. Port must be between 1-65535");
}
if config.workers == 0 {
bail!("Invalid worker count: 0. Must have at least 1 worker");
}
// Expand environment variables
expand_env_vars(&mut config)
.context("Failed to expand environment variables in config")?;
Ok(config)
}
fn try_load_from_file(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {}", path))
}
use anyhow::{Context, Result};
async fn aggregate_user_data(user_id: u64) -> Result<AggregatedData> {
// Call multiple services with context
let profile = fetch_user_profile(user_id)
.await
.context("Failed to fetch user profile from user-service")?;
let orders = fetch_user_orders(user_id)
.await
.context("Failed to fetch orders from order-service")?;
let recommendations = fetch_recommendations(user_id)
.await
.context("Failed to fetch recommendations from ml-service")?;
Ok(AggregatedData {
profile,
orders,
recommendations,
})
}
async fn fetch_user_profile(user_id: u64) -> Result<Profile> {
let response = http_client
.get(&format!("http://user-service/users/{}", user_id))
.send()
.await
.with_context(|| format!("HTTP request failed for user {}", user_id))?;
if !response.status().is_success() {
bail!("User service returned status {}", response.status());
}
response.json()
.await
.context("Failed to parse profile JSON")
}
use anyhow::{Context as _, Result};
trait ResultExt<T> {
fn with_field_context(self, field: &str) -> Result<T>;
fn with_path_context(self, path: &std::path::Path) -> Result<T>;
}
impl<T, E: std::error::Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
fn with_field_context(self, field: &str) -> Result<T> {
self.with_context(|| format!("Invalid value for field '{}'", field))
}
fn with_path_context(self, path: &std::path::Path) -> Result<T> {
self.with_context(|| format!("Error processing file: {}", path.display()))
}
}
// Usage
fn parse_config_field(value: &str) -> Result<u32> {
value.parse().with_field_context("port")
}
fn print_error_chain(mut err: &dyn std::error::Error) {
eprintln!("Error: {}", err);
while let Some(source) = err.source() {
eprintln!(" Caused by: {}", source);
err = source;
}
}
// With anyhow
fn print_anyhow_chain(err: &anyhow::Error) {
eprintln!("Error: {:?}", err); // Debug shows full chain
// Or manually
for (i, cause) in err.chain().enumerate() {
if i == 0 {
eprintln!("Error: {}", cause);
} else {
eprintln!(" Caused by: {}", cause);
}
}
}
// Good: Each layer adds its own context
fn process_payment() -> Result<()> {
charge_card()
.context("Failed to charge credit card")?;
update_database()
.context("Failed to update payment records")?;
send_receipt()
.context("Failed to send receipt email")?;
Ok(())
}
// Good: Context includes IDs, paths, etc.
user_service.get_user(user_id)
.with_context(|| format!("Failed to fetch user with ID {}", user_id))?;
// Bad: Doesn't help debugging
operation().context("An error occurred")?;
// Good: Specific context
operation().context("Failed to initialize database connection pool")?;
// Bad: Repeats error message
std::fs::read_to_string(path)
.context(format!("No such file or directory: {}", path))?;
// Good: Adds new context
std::fs::read_to_string(path)
.with_context(|| format!("Failed to load config from {}", path))?;
#[derive(Debug)]
pub struct ErrorContext {
operation: String,
user_id: Option<u64>,
request_id: Option<String>,
timestamp: std::time::SystemTime,
}
impl ErrorContext {
pub fn new(operation: impl Into<String>) -> Self {
Self {
operation: operation.into(),
user_id: None,
request_id: None,
timestamp: std::time::SystemTime::now(),
}
}
pub fn with_user(mut self, user_id: u64) -> Self {
self.user_id = Some(user_id);
self
}
pub fn with_request_id(mut self, request_id: String) -> Self {
self.request_id = Some(request_id);
self
}
}
// Usage
fn process_request(user_id: u64, request_id: String) -> Result<()> {
let ctx = ErrorContext::new("process_user_request")
.with_user(user_id)
.with_request_id(request_id);
perform_operation()
.with_context(|| format!("{:?}", ctx))?;
Ok(())
}
Build error handling for API gateway that calls 3+ microservices.
Hints:Create pipeline that reads, transforms, validates, writes - with context at each stage.
Hints:Implement database transaction with context for each operation.
Hints:Uses context for build error reporting.
View on GitHubORM with detailed query error context.
View on GitHubDatabase library with query context in errors.
View on GitHubRun this code in the official Rust Playground