How does anyhow::Error::new wrap custom error types while preserving their std::error::Error implementation?

anyhow::Error::new wraps any type implementing std::error::Error plus Send + Sync + 'static into a heap-allocated ErrorImpl that stores the error alongside a vtable for dynamic dispatch, preserving access to the original error's methods through type-erased interfaces. The wrapped error can be downcast back to its original type, have its source() chain traversed, and be formatted with all its original details. This works because anyhow::Error internally stores a Box<ErrorImpl<E>> where E is the concrete error type, and the vtable provides methods like source(), downcast(), and fmt::Display that delegate to the wrapped type's implementations.

Basic anyhow::Error::new Usage

use anyhow::{Error, Result};
 
// Define a custom error type
#[derive(Debug)]
struct DatabaseError {
    message: String,
}
 
impl std::fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Database error: {}", self.message)
    }
}
 
impl std::error::Error for DatabaseError {}
 
fn basic_wrapping() {
    // Wrap a custom error type
    let db_error = DatabaseError {
        message: "Connection failed".to_string(),
    };
    
    // anyhow::Error::new wraps the error
    let anyhow_error = Error::new(db_error);
    
    // Display works correctly
    println!("Error: {}", anyhow_error);
    // Output: Error: Database error: Connection failed
    
    // The wrapped error's Display implementation is preserved
}

Error::new takes any error type and wraps it, preserving its Display implementation.

The Type Requirements

use anyhow::Error;
 
// Error::new requires: std::error::Error + Send + Sync + 'static
 
#[derive(Debug)]
struct MyError {
    code: u32,
}
 
impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error code: {}", self.code)
    }
}
 
impl std::error::Error for MyError {}
 
fn type_requirements() {
    // This works: MyError implements Error + Send + Sync + 'static
    let error = Error::new(MyError { code: 404 });
    
    // These would NOT compile:
    // - Types without std::error::Error impl
    // - Types that are !Send or !Sync
    // - Types with non-'static references
    
    // Example of what's NOT allowed:
    // struct RefError<'a>(&'a str);  // Not 'static
    // impl std::error::Error for RefError<'_> {}
    // let e = Error::new(RefError(&s));  // Won't compile: needs 'static
}

The wrapped type must implement std::error::Error, Send, Sync, and 'static.

Internal Representation: Box

use anyhow::Error;
 
// Simplified view of anyhow's internal structure:
// 
// pub struct Error {
//     inner: Box<ErrorImpl<dyn StdError>>,
// }
// 
// Where ErrorImpl<E> stores:
// - The error object: E (the actual error type)
// - A vtable for dynamic dispatch
 
#[derive(Debug)]
struct IoError {
    message: String,
}
 
impl std::fmt::Display for IoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "IO failed: {}", self.message)
    }
}
 
impl std::error::Error for IoError {}
 
fn internal_structure() {
    let io_error = IoError {
        message: "File not found".to_string(),
    };
    
    // Error::new creates Box<ErrorImpl<IoError>>
    // This box:
    // 1. Allocates on heap (error may be large)
    // 2. Stores IoError and its vtable
    // 3. Returns fat pointer to dyn StdError
    
    let error: Error = Error::new(io_error);
    
    // The heap allocation allows:
    // - Different sized errors to be stored uniformly
    // - Error type to be erased (anyhow::Error is one type)
    // - Vtable to provide dynamic dispatch
}

The error is heap-allocated with a vtable for type-erased dynamic dispatch.

Preserving std::error::Error Methods

use anyhow::Error;
use std::error::Error as StdError;
 
#[derive(Debug)]
struct ChainError {
    message: String,
    source: Option<Box<dyn StdError + Send + Sync>>,
}
 
impl std::fmt::Display for ChainError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}
 
impl StdError for ChainError {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        self.source.as_ref().map(|e| e.as_ref() as &dyn StdError)
    }
}
 
fn preserved_methods() {
    let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
    let chain_error = ChainError {
        message: "Operation failed".to_string(),
        source: Some(Box::new(inner)),
    };
    
    // Wrap the chain error
    let error = Error::new(chain_error);
    
    // source() is preserved and accessible
    if let Some(source) = error.source() {
        println!("Caused by: {}", source);
    }
    
    // The entire error chain is preserved
    let mut current: &dyn StdError = &error;
    while let Some(cause) = current.source() {
        println!("Caused by: {}", cause);
        current = cause;
    }
}

The source() method from std::error::Error is preserved and works through the wrapper.

Downcasting to Original Type

use anyhow::Error;
 
#[derive(Debug)]
struct ConfigError {
    file: String,
    line: u32,
}
 
impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Config error at {}:{}
", self.file, self.line)
    }
}
 
impl std::error::Error for ConfigError {}
 
fn downcasting() {
    let config_error = ConfigError {
        file: "config.toml".to_string(),
        line: 42,
    };
    
    let error: Error = Error::new(config_error);
    
    // Downcast to original type
    if let Some(config_err) = error.downcast_ref::<ConfigError>() {
        println!("File: {}, Line: {}", config_err.file, config_err.line);
    }
    
    // Downcast consuming self
    let error2 = Error::new(ConfigError {
        file: "app.toml".to_string(),
        line: 10,
    });
    
    let recovered: ConfigError = error2.downcast::<ConfigError>().unwrap();
    println!("Recovered: {} at line {}", recovered.file, recovered.line);
}

downcast_ref() and downcast() retrieve the original error type from the wrapper.

The Vtable Mechanism

use anyhow::Error;
 
// When Error::new wraps an error, it creates a vtable with:
// - display: fn(&self, &mut Formatter) -> fmt::Result
// - debug: fn(&self, &mut Formatter) -> fmt::Result
// - source: fn(&self) -> Option<&dyn Error>
// - downcast: fn(self: Box<Self>, TypeId) -> Result<Box<dyn Any>, Box<dyn Error>>
// - etc.
 
#[derive(Debug)]
struct NetworkError {
    code: i32,
}
 
impl std::fmt::Display for NetworkError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Network error code {}", self.code)
    }
}
 
impl std::error::Error for NetworkError {}
 
fn vtable_explanation() {
    let network_error = NetworkError { code: 500 };
    
    // Error::new creates vtable pointing to NetworkError's implementations
    let error: Error = Error::new(network_error);
    
    // When you call:
    // - error.to_string() -> vtable.display()
    // - error.source() -> vtable.source()
    // - error.downcast_ref() -> vtable.downcast_ref()
    
    // The vtable ensures NetworkError's implementations are called,
    // not some generic default
    
    let message = format!("{}", error);  // Uses NetworkError's Display
    assert_eq!(message, "Network error code 500");
}

The vtable delegates calls to the original error type's implementations.

Error Chains with .context()

use anyhow::{Context, Error, Result};
 
#[derive(Debug)]
struct ParseError {
    position: usize,
}
 
impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Parse error at position {}", self.position)
    }
}
 
impl std::error::Error for ParseError {}
 
fn error_chains() -> Result<()> {
    // Create a base error
    let parse_error = ParseError { position: 42 };
    
    // Wrap and add context
    let error = Error::new(parse_error).context("Failed to parse configuration");
    
    // The context creates an error chain:
    // 1. "Failed to parse configuration"
    // 2. "Parse error at position 42"
    
    // source() traverses the chain
    let mut current: &dyn std::error::Error = &error;
    println!("Error: {}", current);
    
    while let Some(cause) = current.source() {
        println!("Caused by: {}", cause);
        current = cause;
    }
    
    // Output:
    // Error: Failed to parse configuration
    // Caused by: Parse error at position 42
    
    Err(error)
}

.context() creates error chains that preserve the original error as a source.

Comparison with Box

use anyhow::Error;
use std::error::Error as StdError;
 
#[derive(Debug)]
struct AppError {
    details: String,
}
 
impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.details)
    }
}
 
impl StdError for AppError {}
 
fn comparison_with_box() {
    let app_error = AppError {
        details: "Something went wrong".to_string(),
    };
    
    // Box<dyn Error>: Standard library approach
    let boxed: Box<dyn StdError + Send + Sync> = Box::new(app_error);
    
    // anyhow::Error: Enhanced error handling
    let anyhow_err = Error::new(AppError {
        details: "Something went wrong".to_string(),
    });
    
    // Key differences:
    
    // 1. Downcasting
    // Box<dyn Error>: needs dynamic type checking
    let _ = boxed.downcast_ref::<AppError>();  // Works but verbose
    
    // anyhow::Error: cleaner API
    let _ = anyhow_err.downcast_ref::<AppError>();
    
    // 2. Context
    // Box<dyn Error>: no built-in context support
    // anyhow::Error: .context() for adding information
    
    // 3. Construction
    // Box<dyn Error>: requires Box::new()
    // anyhow::Error: Error::new() or .into()
}

anyhow::Error provides a cleaner API over Box<dyn Error> with additional features.

The Error Trait Implementation

use anyhow::Error;
use std::error::Error as StdError;
 
fn error_trait_impl() {
    // anyhow::Error implements std::error::Error
    // This means it can be used anywhere StdError is expected
    
    let error = Error::msg("Something failed");
    
    // Can be used as &dyn StdError
    let std_error: &dyn StdError = &error;
    println!("As StdError: {}", std_error);
    
    // Can be used in error chains
    let wrapped = Error::new(error).context("While processing");
    
    // source() works through the chain
    if let Some(source) = wrapped.source() {
        println!("Source: {}", source);
    }
}

anyhow::Error itself implements std::error::Error, so it integrates with the standard error ecosystem.

Creating Errors with Error::new vs Error::msg

use anyhow::{Error, anyhow, bail};
 
#[derive(Debug)]
struct ValidationError {
    field: String,
}
 
impl std::fmt::Display for ValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Validation failed for field '{}'", self.field)
    }
}
 
impl std::error::Error for ValidationError {}
 
fn creation_methods() {
    // Error::new: Wrap a custom error type
    let custom_error = Error::new(ValidationError {
        field: "email".to_string(),
    });
    
    // Error::msg: Create a simple message error
    let message_error = Error::msg("Something went wrong");
    
    // anyhow! macro: Format a message error
    let formatted_error = anyhow!("Error code: {}", 500);
    
    // bail! macro: Return early with an error
    // bail!("Critical failure");  // Returns Err(Error)
    
    // All produce anyhow::Error, but:
    // - Error::new preserves custom type (can downcast)
    // - Error::msg creates simple message (no custom type)
    
    // With custom error:
    if let Some(validation_err) = custom_error.downcast_ref::<ValidationError>() {
        println!("Field: {}", validation_err.field);
    }
    
    // With message error:
    // No custom type to downcast to
}

Error::new preserves the custom type; Error::msg creates a simple string error.

Debug Formatting Preservation

use anyhow::Error;
 
#[derive(Debug)]
struct DetailedError {
    code: u32,
    message: String,
    context: Vec<String>,
}
 
impl std::fmt::Display for DetailedError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error {}: {}", self.code, self.message)
    }
}
 
impl std::error::Error for DetailedError {}
 
fn debug_preservation() {
    let error = DetailedError {
        code: 500,
        message: "Internal error".to_string(),
        context: vec
!["request.json".to_string(), "user_id: 123".to_string()],
    };
    
    let wrapped = Error::new(error);
    
    // Display: uses original's Display
    println!("Display: {}", wrapped);
    // Output: Error 500: Internal error
    
    // Debug: uses original's Debug
    println!("Debug: {:?}", wrapped);
    // Output: DetailedError { code: 500, message: "Internal error", context: ["request.json", "user_id: 123"] }
    
    // {:?} shows the full Debug output of the wrapped error
}

Both Display and Debug implementations are preserved through the wrapper.

Converting Errors Automatically with .into()

use anyhow::{Error, Result};
 
#[derive(Debug)]
struct AuthError {
    user: String,
}
 
impl std::fmt::Display for AuthError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Authentication failed for user '{}'", self.user)
    }
}
 
impl std::error::Error for AuthError {}
 
// Manual wrapping with Error::new
fn manual_wrapping() -> Result<()> {
    let auth_error = AuthError {
        user: "admin".to_string(),
    };
    Err(Error::new(auth_error))
}
 
// Automatic conversion with .into()
fn automatic_conversion() -> Result<()> {
    let auth_error = AuthError {
        user: "admin".to_string(),
    };
    // Into<anyhow::Error> is implemented for types with std::error::Error
    Err(auth_error.into())
}
 
// Using ? operator (also uses Into)
fn with_question_mark() -> Result<()> {
    // Function that returns AuthError
    fn authenticate() -> Result<(), AuthError> {
        Err(AuthError {
            user: "guest".to_string(),
        })
    }
    
    // ? converts AuthError -> anyhow::Error automatically
    authenticate()?;
    Ok(())
}

The From<E> implementation for anyhow::Error allows automatic conversion.

Multiple Error Types in One Function

use anyhow::{Error, Result};
use std::fs::File;
use std::io::Read;
 
#[derive(Debug)]
struct ConfigError {
    message: String,
}
 
impl std::fmt::Display for ConfigError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Config error: {}", self.message)
    }
}
 
impl std::error::Error for ConfigError {}
 
fn multiple_error_types() -> Result<()> {
    // Different error types, all converted to anyhow::Error
    
    // std::io::Error -> anyhow::Error
    let mut file = File::open("config.toml")?;
    
    // Custom ConfigError -> anyhow::Error
    let mut contents = String::new();
    file.read_to_string(&mut contents)
.map_err(|e| Error::new(e))?;
    
    // Manual conversion
    if contents.is_empty() {
        return Err(Error::new(ConfigError {
            message: "Empty config file".to_string(),
        }));
    }
    
    Ok(())
}

anyhow::Error unifies different error types through automatic conversion.

Accessing the Inner Error

use anyhow::Error;
use std::error::Error as StdError;
 
#[derive(Debug)]
struct TimeoutError {
    duration_ms: u64,
}
 
impl std::fmt::Display for TimeoutError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Operation timed out after {}ms", self.duration_ms)
    }
}
 
impl StdError for TimeoutError {}
 
fn accessing_inner() {
    let error = Error::new(TimeoutError { duration_ms: 5000 });
    
    // Method 1: downcast_ref (borrow)
    if let Some(timeout) = error.downcast_ref::<TimeoutError>() {
        println!("Timeout duration: {}ms", timeout.duration_ms);
    }
    
    // Method 2: downcast (consume)
    let error2 = Error::new(TimeoutError { duration_ms: 3000 });
    match error2.downcast::<TimeoutError>() {
        Ok(timeout) => println!("Recovered timeout: {}ms", timeout.duration_ms),
        Err(original_error) => println!("Not a TimeoutError: {}", original_error),
    }
    
    // Method 3: as inner dyn Error
    let error3 = Error::new(TimeoutError { duration_ms: 1000 });
    let inner: &dyn StdError = error3.inner();
    println!("Inner error: {}", inner);
}

Multiple methods exist to access the wrapped error depending on your needs.

Real-World Usage Pattern

use anyhow::{Context, Error, Result};
use std::error::Error as StdError;
 
// Define domain errors
#[derive(Debug)]
pub struct DatabaseError {
    pub query: String,
    pub reason: String,
}
 
impl std::fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Database error on query '{}': {}", self.query, self.reason)
    }
}
 
impl StdError for DatabaseError {}
 
#[derive(Debug)]
pub struct ValidationError {
    pub field: String,
    pub value: String,
    pub constraint: String,
}
 
impl std::fmt::Display for ValidationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Validation failed: field '{}' with value '{}' violates {}",
            self.field, self.value, self.constraint
        )
    }
}
 
impl StdError for ValidationError {}
 
// Application function using anyhow
fn process_user(id: u32, name: &str) -> Result<()> {
    // Validate input
    if name.is_empty() {
        return Err(Error::new(ValidationError {
            field: "name".to_string(),
            value: name.to_string(),
            constraint: "non-empty".to_string(),
        }));
    }
    
    // Database operation with context
    fn query_database(id: u32) -> Result<String, DatabaseError> {
        Err(DatabaseError {
            query: format!("SELECT * FROM users WHERE id = {}", id),
            reason: "Connection refused".to_string(),
        })
    }
    
    // Convert domain error to anyhow with context
    let result = query_database(id)
        .map_err(Error::new)
        .context(format!("Failed to fetch user {}", id))?;
    
    Ok(())
}
 
fn handle_errors() {
    match process_user(1, "") {
        Ok(()) => println!("Success"),
        Err(e) => {
            // Try to downcast to specific error types
            if let Some(validation) = e.downcast_ref::<ValidationError>() {
                println!("Validation error on field: {}", validation.field);
            } else if let Some(db) = e.downcast_ref::<DatabaseError>() {
                println!("Database error: {}", db.reason);
            } else {
                println!("Unknown error: {}", e);
            }
            
            // Print full chain
            let mut current: &dyn StdError = &*e;
            println!("Error chain:");
            println!("  - {}", current);
            while let Some(cause) = current.source() {
                println!("  - {}", cause);
                current = cause;
            }
        }
    }
}

Real applications use domain-specific errors wrapped in anyhow::Error with context.

Synthesis

How wrapping works:

// Error::new creates:
// 1. Heap allocation for the error
// 2. Vtable pointing to the error's implementations
// 3. Type-erased reference stored in anyhow::Error
 
let error = Error::new(MyCustomError { ... });
 
// Equivalent to:
// let boxed: Box<dyn StdError + Send + Sync> = Box::new(MyCustomError { ... });
// But with:
// - Downcasting support
// - Context chaining
// - Cleaner API

Key preservation points:

// What's preserved from the wrapped error:
// 1. Display implementation -> format!("{}", error)
// 2. Debug implementation -> format!("{:?}", error)
// 3. source() method -> error.source()
// 4. Type information -> error.downcast_ref::<T>()
 
// What's NOT preserved:
// - Direct field access (must downcast)
// - Private methods (can't call on anyhow::Error)
// - Non-error trait implementations

Vtable mechanism:

// Internal structure (simplified):
struct ErrorVtable {
    display: fn(&dyn Any, &mut Formatter) -> fmt::Result,
    debug: fn(&dyn Any, &mut Formatter) -> fmt::Result,
    source: fn(&dyn Any) -> Option<&dyn Error>,
    downcast: fn(Box<dyn Any>, TypeId) -> Result<Box<T>, Box<dyn Error>>,
}
 
// When Error::new::<T>(error):
// - Creates vtable with T's implementations
// - Stores T in Box<ErrorImpl<T>>
// - Returns type-erased Error

Key insight: anyhow::Error::new wraps custom error types by boxing them with a vtable that preserves their std::error::Error implementation—specifically their Display, Debug, and source() methods—while providing downcasting capabilities to recover the original type. The wrapper itself implements std::error::Error, making it compatible with the standard error ecosystem. This design allows anyhow::Error to unify different error types while still enabling type-specific error handling through downcasting. The heap allocation is necessary because anyhow::Error must be a single type regardless of what's wrapped inside, and the vtable enables dynamic dispatch to the original type's implementations without knowing the concrete type at compile time.