What is the purpose of anyhow::Chain for iterating through the causes of an error?
anyhow::Chain provides an iterator over the chain of underlying causes in an anyhow::Error, allowing you to traverse from the top-level error down through each nested error in the causal chain. This enables introspection of error sources for logging, debugging, or extracting specific error types from deeply nested errors, which is essential when errors are wrapped with additional context using .context() or .with_context().
The Error Chain Problem
use anyhow::{Context, Error, Result};
fn demonstrate_chain() -> Result<()> {
// Errors often have multiple layers of context
let result = read_config()
.context("Failed to initialize application")
.context("Application startup failed");
if let Err(e) = result {
// The error has a chain:
// 1. "Application startup failed" (outermost)
// 2. "Failed to initialize application" (middle)
// 3. The original IO error (root cause)
println!("Error: {}", e);
// Prints: "Application startup failed"
// Only shows the outermost error!
}
Ok(())
}
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to read config file")
}The default error display only shows the outermost context, hiding the root cause.
Accessing the Chain with chain()
use anyhow::{Context, Error, Result};
fn basic_chain_access() -> Result<()> {
let err = std::fs::read_to_string("nonexistent.txt")
.context("Failed to load file")
.context("Failed to initialize")
.unwrap_err();
// Get the chain iterator
let chain = err.chain();
// Iterate through all errors in the chain
for (i, cause) in chain.enumerate() {
println!("Cause {}: {}", i, cause);
}
// Output:
// Cause 0: Failed to initialize
// Cause 1: Failed to load file
// Cause 2: No such file or directory (os error 2)
Ok(())
}chain() returns a Chain iterator that yields each error in the causal sequence.
Chain Iterator Behavior
use anyhow::{Context, Result};
fn chain_iterator_behavior() {
let err = inner_function().unwrap_err();
// Chain iterates from outermost to innermost
let causes: Vec<_> = err.chain().collect();
// causes[0] is the outermost context
// causes[last] is the root cause
// Chain implements Iterator
let first_cause = err.chain().next();
println!("First cause: {:?}", first_cause); // Outermost error
// Chain also implements DoubleEndedIterator
let last_cause = err.chain().last();
println!("Root cause: {:?}", last_cause); // Innermost error
// Count total causes
let depth = err.chain().count();
println!("Error depth: {}", depth);
}
fn inner_function() -> Result<()> {
std::fs::read_to_string("missing")
.context("Reading config")
.context("Loading settings")
}Chain iterates from outermost to innermost (most recent context to root cause).
Practical Use Case: Detailed Error Logging
use anyhow::{Context, Result};
fn detailed_logging() -> Result<()> {
let result = process_data().context("Processing failed");
if let Err(err) = result {
// Log just the main error
eprintln!("Error: {}", err);
// Log the full chain for debugging
eprintln!("Error chain:");
for (i, cause) in err.chain().enumerate() {
eprintln!(" [{}] {}", i, cause);
}
// This gives visibility into the full causal path
// Which is crucial for debugging production issues
}
Ok(())
}
fn process_data() -> Result<()> {
parse_file()
.context("Invalid data format")
}
fn parse_file() -> Result<()> {
std::fs::read_to_string("data.json")
.context("File read error")
}Logging the full chain reveals the complete error path.
Finding a Specific Error Type
use anyhow::{Context, Result};
use std::io;
fn find_error_type() {
let err = read_config().unwrap_err();
// Search for a specific error type in the chain
let io_error = err.chain()
.find_map(|e| e.downcast_ref::<io::Error>());
if let Some(io_err) = io_error {
println!("Found IO error: {:?}", io_err);
if io_err.kind() == io::ErrorKind::NotFound {
println!("File not found!");
}
}
// Check if any cause matches a type
let has_io_error = err.chain()
.any(|e| e.is::<io::Error>());
println!("Has IO error: {}", has_io_error);
}
fn read_config() -> Result<String> {
std::fs::read_to_string("config.txt")
.context("Failed to read config")
}Use downcast_ref on chain elements to find specific error types.
Extracting the Root Cause
use anyhow::{Context, Result};
fn root_cause_extraction() {
let err = nested_operation().unwrap_err();
// Get the root cause (innermost error)
let root_cause = err.root_cause();
// root_cause() is equivalent to chain().last()
println!("Root cause: {}", root_cause);
// Compare to chain().last()
let chain_root = err.chain().last().unwrap();
assert!(std::ptr::eq(root_cause, chain_root));
// Root cause is typically the original error
// without any context wrappers
}
fn nested_operation() -> Result<()> {
std::fs::read_to_string("missing")
.context("Reading data")
.context("Processing")
.context("Operation failed")
}root_cause() returns the innermost error in the chain.
Chain with anyhow! Macro
use anyhow::{anyhow, Context, Result};
fn chain_with_macro() {
let err = create_error().unwrap_err();
// Chain includes all wrapped errors
for (i, cause) in err.chain().enumerate() {
println!("{}: {}", i, cause);
}
// 0: Outer wrapper
// 1: Middle wrapper
// 2: Original error: something went wrong
}
fn create_error() -> Result<()> {
Err(anyhow!("Original error: something went wrong"))
.context("Middle wrapper")
.context("Outer wrapper")
}Errors created with anyhow! become the root of the chain when wrapped.
Comparing Display and Debug Formats
use anyhow::{Context, Result};
fn display_vs_debug() {
let err = load_resource().unwrap_err();
// Display shows just the outermost error
println!("Display: {}", err);
// "Failed to load resource"
// Debug shows more context with {:?}
println!("Debug: {:?}", err);
// Shows chain in some format
// Pretty debug with {:#?}
println!("Pretty: {:#?}", err);
// But for full control, use chain()
println!("Full chain:");
for cause in err.chain() {
println!(" - {}", cause);
}
// Alternative: use the format chain
// anyhow provides Display for the full chain
println!("Full error: {:#}", err);
// {:#} shows all causes on separate lines
}
fn load_resource() -> Result<()> {
std::fs::read_to_string("resource.txt")
.context("File error")
.context("Failed to load resource")
}{:#} format specifier shows the full chain; custom iteration gives more control.
Finding Errors by Pattern
use anyhow::{Context, Result};
fn pattern_matching() {
let err = operation().unwrap_err();
// Find error containing specific text
let not_found = err.chain()
.find(|cause| cause.to_string().contains("not found"));
if let Some(cause) = not_found {
println!("Found 'not found' error: {}", cause);
}
// Count errors in chain
let depth = err.chain().count();
println!("Error depth: {}", depth);
// Check if error chain contains a pattern
let has_io_error = err.chain()
.any(|cause| cause.to_string().contains("IO"));
println!("Has IO error: {}", has_io_error);
}
fn operation() -> Result<()> {
std::fs::read_to_string("missing")
.context("IO operation failed")
}Use iterator methods on Chain to search for patterns or specific content.
Chain Length and Depth
use anyhow::{Context, Result};
fn chain_depth() {
let err = level_3().unwrap_err();
// Count errors in chain
let count = err.chain().count();
println!("Chain length: {}", count); // 3
// Check if chain has multiple causes
if count > 1 {
println!("This is a wrapped error with {} levels", count);
}
// Access by index (requires collecting)
let causes: Vec<_> = err.chain().collect();
println!("Level 0: {}", causes[0]);
println!("Level 1: {}", causes[1]);
println!("Level 2: {}", causes[2]);
}
fn level_1() -> Result<()> {
std::fs::read_to_string("missing")
.context("Level 1")
}
fn level_2() -> Result<()> {
level_1().context("Level 2")
}
fn level_3() -> Result<()> {
level_2().context("Level 3")
}chain().count() tells you how many error layers exist.
Real-World Example: API Error Handling
use anyhow::{Context, Result};
use std::io;
fn api_error_handling() {
let result = fetch_user_data(123);
if let Err(err) = result {
// Log full chain for debugging
log_error_chain(&err);
// Determine appropriate HTTP status based on root cause
let status = determine_status(&err);
println!("HTTP Status: {}", status);
}
}
fn log_error_chain(err: &anyhow::Error) {
eprintln!("Error chain:");
for (i, cause) in err.chain().enumerate() {
eprintln!(" [{}] {}", i, cause);
}
}
fn determine_status(err: &anyhow::Error) -> u16 {
// Look for specific error types in chain
if err.chain().any(|e| e.to_string().contains("not found")) {
return 404;
}
if err.chain().any(|e| e.is::<io::Error>()) {
return 500;
}
// Check root cause type
let root = err.root_cause();
if root.to_string().contains("permission denied") {
return 403;
}
500 // Default to internal server error
}
fn fetch_user_data(user_id: u32) -> Result<String> {
let path = format!("data/users/{}.json", user_id);
std::fs::read_to_string(&path)
.with_context(|| format!("Failed to fetch user {}", user_id))
.context("User data retrieval failed")
}Use chain inspection for error classification and appropriate responses.
Converting Chain to Collection
use anyhow::{Context, Result};
fn chain_to_collection() {
let err = create_chain().unwrap_err();
// Collect into Vec
let causes: Vec<_> = err.chain().collect();
// Get first few causes
let first_three: Vec<_> = err.chain().take(3).collect();
// Skip the outermost wrapper
let inner_causes: Vec<_> = err.chain().skip(1).collect();
// Create error summary
let summary: String = err.chain()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(" -> ");
println!("Error path: {}", summary);
// "Outer -> Middle -> Inner -> Root cause"
}
fn create_chain() -> Result<()> {
Err(anyhow::anyhow!("Root cause"))
.context("Inner")
.context("Middle")
.context("Outer")
}Collect the chain for indexed access or string manipulation.
Chain vs source()
use anyhow::{Context, Result};
use std::error::Error as StdError;
fn chain_vs_source() {
let err = nested_error().unwrap_err();
// chain() iterates all causes
println!("Using chain():");
for cause in err.chain() {
println!(" {}", cause);
}
// source() returns the immediate cause (requires std::error::Error trait)
// anyhow::Error implements std::error::Error
println!("\nUsing source():");
let mut current: Option<&dyn StdError> = Some(&err);
while let Some(e) = current {
println!(" {}", e);
current = e.source();
}
// chain() is more ergonomic and type-safe
// source() requires working with trait objects
}
fn nested_error() -> Result<()> {
std::fs::read_to_string("missing")
.context("Reading file")
.context("Operation failed")
}chain() is the anyhow-specific iterator; source() is the standard library approach.
Downcasting Through the Chain
use anyhow::{Context, Result};
use std::io;
fn downcast_chain() {
let err = read_config().unwrap_err();
// Find and downcast to specific type
for cause in err.chain() {
if let Some(io_err) = cause.downcast_ref::<io::Error>() {
println!("Found IO error with kind: {:?}", io_err.kind());
break;
}
}
// Alternative: use find_map
let io_error = err.chain()
.find_map(|e| e.downcast_ref::<io::Error>());
if let Some(io_err) = io_error {
match io_err.kind() {
io::ErrorKind::NotFound => println!("File not found"),
io::ErrorKind::PermissionDenied => println!("Permission denied"),
_ => println!("Other IO error: {}", io_err),
}
}
// Check for custom error types
// if let Some(custom) = err.chain().find_map(|e| e.downcast_ref::<MyError>()) {
// // Handle custom error
// }
}
fn read_config() -> Result<String> {
std::fs::read_to_string("config.toml")
.context("Failed to load configuration")
}Downcast through the chain to access typed error information.
Summary Table
fn summary() {
// | Method | Returns | Direction | Use Case |
// |----------------|--------------------|--------------------|-----------------------|
// | chain() | Chain iterator | Outer to inner | Inspect all causes |
// | root_cause() | &(dyn Error) | Innermost only | Get root error |
// | source() | Option<&dyn Error> | One level deeper | Standard Error trait |
// | Chain Method | Description |
// |----------------|---------------------------------------|
// | next() | Get outermost cause (first in chain) |
// | last() | Get root cause (last in chain) |
// | count() | Number of causes in chain |
// | nth(n) | Get specific cause by index |
// | find_map() | Find and transform a cause |
// | Format | Output |
// |----------------|--------------------------------------|
// | {} | Outermost error only |
// | {:?} | Debug format (varies) |
// | {:#} | All causes on separate lines |
// | chain() | Full control over presentation |
}Synthesis
Quick reference:
use anyhow::{Context, Result};
let err = operation().unwrap_err();
// Iterate all causes (outermost to innermost)
for cause in err.chain() {
println!("{}", cause);
}
// Get root cause directly
let root = err.root_cause();
println!("Root: {}", root);
// Find specific error type
let io_error = err.chain()
.find_map(|e| e.downcast_ref::<std::io::Error>());
// Print full chain with {:#}
println!("{:#}", err);
// Count error depth
let depth = err.chain().count();Key insight: anyhow::Chain solves the fundamental problem of error introspection in error chains created by .context() calls. When you add context to an error, anyhow wraps it in layersâthe outermost context is visible with {} display, but the root cause and intermediate contexts are hidden. The chain() method provides a type-safe iterator that yields each error in sequence from outermost to innermost, enabling you to log the full causal path, find specific error types with downcast_ref, extract the root cause with root_cause() or chain().last(), and build custom error reports. Without Chain, you'd have to use the std::error::Error::source() method which returns Option<&dyn Error> and requires manual iteration with trait objects. Chain is more ergonomic because it integrates directly with anyhow::Error and provides standard iterator methods like find_map, any, and take. The practical value is in debugging production issues: logging just the top-level error hides critical information about what actually failed, while iterating the full chain reveals the complete path from user-facing message to root cause. Use err.chain() for detailed logging, err.root_cause() for quick root cause access, and {:#} format for a built-in multi-line display of all causes.
