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.