What are the trade-offs between anyhow::Error::downcast and downcast_ref for type-based error inspection?

downcast moves the error and returns Result<T, Error>, consuming the original error on failure and allowing recovery of the inner value on success, while downcast_ref borrows the error and returns Option<&T>, preserving the original error but only providing a reference to the inner value. The fundamental trade-off is ownership: downcast lets you extract and own the inner value but consumes the error, while downcast_ref lets you inspect without consuming but you can't take ownership of the inner value.

The anyhow::Error Type

use anyhow::{Error, anyhow};
 
fn error_basics() {
    // anyhow::Error is a trait object wrapper
    // It can hold any error type implementing std::error::Error + Send + Sync + 'static
    
    let error: Error = anyhow!("Something went wrong");
    
    // It can also wrap concrete error types
    let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
    let error: Error = Error::new(io_error);
    
    // The concrete type is preserved internally
    // You can attempt to extract it with downcast methods
}

anyhow::Error wraps concrete error types while preserving their original type for downcasting.

The downcast Method

use anyhow::{Error, anyhow};
 
fn downcast_example() {
    // Create an error wrapping a concrete type
    let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
    let error: Error = Error::new(io_error);
    
    // downcast CONSUMES the error and tries to extract the inner type
    let result: Result<std::io::Error, Error> = error.downcast::<std::io::Error>();
    
    match result {
        Ok(io_error) => {
            // We OWN the inner error now
            // Can move it, return it, etc.
            println!("Got io::Error: {}", io_error);
        }
        Err(original_error) => {
            // Type didn't match
            // We get back the original error (ownership preserved)
            println!("Not an io::Error");
        }
    }
}

downcast consumes self and returns Result<T, Error>—you either get the inner value or the original error back.

The downcast_ref Method

use anyhow::{Error, anyhow};
 
fn downcast_ref_example() {
    // Create an error wrapping a concrete type
    let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
    let error: Error = Error::new(io_error);
    
    // downcast_ref BORROWS the error and returns an Option<&T>
    let result: Option<&std::io::Error> = error.downcast_ref::<std::io::Error>();
    
    if let Some(io_error) = result {
        // We have a REFERENCE to the inner error
        // Cannot move it, but can inspect it
        println!("Got io::Error: {}", io_error);
        println!("Kind: {:?}", io_error.kind());
    }
    
    // error is still valid - we only borrowed it
    println!("Original error still valid: {}", error);
}

downcast_ref borrows &self and returns Option<&T>—you get a reference but keep the original error.

Key Difference: Ownership Transfer

use anyhow::Error;
 
fn ownership_comparison() {
    // Scenario: You need the inner error for further processing
    
    // With downcast: You OWN the inner value
    fn take_ownership(error: Error) -> Result<std::io::Error, Error> {
        match error.downcast::<std::io::Error>() {
            Ok(io_error) => {
                // We own io_error, can return it, store it, etc.
                Ok(io_error)
            }
            Err(e) => Err(e),
        }
    }
    
    // With downcast_ref: You only BORROW
    fn take_reference(error: &Error) -> Option<&std::io::Error> {
        // error.downcast_ref::<std::io::Error>()
        // Returns Option<&std::io::Error>
        // Cannot take ownership of the io::Error
        error.downcast_ref::<std::io::Error>()
    }
    
    // Key insight:
    // - downcast gives ownership, but consumes the Error
    // - downcast_ref gives reference, but preserves the Error
}

The core trade-off: ownership of inner value versus preservation of outer error.

Key Difference: Error Preservation

use anyhow::Error;
 
fn error_preservation() {
    let error: Error = Error::new(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "file not found"
    ));
    
    // downcast: On failure, error is moved back to caller
    let result = error.downcast::<String>();  // Wrong type
    
    match result {
        Ok(_s) => println!("Got a String (impossible)"),
        Err(original_error) => {
            // original_error is the same error we had
            // We can continue using it
            println!("Not a String: {}", original_error);
        }
    }
    
    // downcast_ref: Error is never moved
    let error: Error = Error::new(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "file not found"
    ));
    
    let result = error.downcast_ref::<String>();  // Wrong type
    
    // error is still valid regardless of result
    println!("Error still valid: {}", error);
}

Both methods preserve the error on failure—downcast returns it in Err, downcast_ref never touched it.

Mutable Access with downcast_mut

use anyhow::Error;
 
fn downcast_mut_example() {
    // There's also downcast_mut for mutable references
    
    let mut error: Error = Error::new(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "file not found"
    ));
    
    // downcast_mut returns Option<&mut T>
    if let Some(io_error) = error.downcast_mut::<std::io::Error>() {
        // We have a mutable reference
        // Can modify the inner error
        *io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
    }
    
    // The error is now modified
    println!("Modified error: {}", error);
}

downcast_mut provides mutable access to the inner error while preserving the outer error.

Practical Use Case: Error Handling Logic

use anyhow::{Error, Result};
 
fn handle_file_error(error: Error) -> Result<()> {
    // Different handling based on error type
    
    // Using downcast_ref for inspection without consuming
    if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
        match io_error.kind() {
            std::io::ErrorKind::NotFound => {
                println!("File not found, creating...");
                // Handle not found
                return Err(error);  // Pass through
            }
            std::io::ErrorKind::PermissionDenied => {
                println!("Permission denied");
                return Err(error);  // Pass through
            }
            _ => {}
        }
    }
    
    // If we didn't handle it, return the original error
    Err(error)
}
 
fn handle_and_recover(error: Error) -> Result<String> {
    // Using downcast when we want to extract and use the inner error
    
    match error.downcast::<std::io::Error>() {
        Ok(io_error) => {
            // We OWN the io::Error now
            // Can use it without the anyhow wrapper
            if io_error.kind() == std::io::ErrorKind::NotFound {
                Ok("Default value".to_string())
            } else {
                Err(Error::new(io_error))  // Re-wrap if needed
            }
        }
        Err(original) => Err(original),
    }
}

Use downcast_ref for inspection; use downcast when you need ownership of the inner value.

Practical Use Case: Multiple Downcasts

use anyhow::{Error, Result};
 
fn multiple_downcasts(error: &Error) {
    // When checking multiple types, downcast_ref is essential
    // Because it doesn't consume the error
    
    if error.downcast_ref::<std::io::Error>().is_some() {
        println!("It's an io::Error");
    } else if error.downcast_ref::<serde_json::Error>().is_some() {
        println!("It's a serde_json::Error");
    } else if error.downcast_ref::<reqwest::Error>().is_some() {
        println!("It's a reqwest::Error");
    }
    
    // With downcast, you'd need to handle ownership:
    // This won't work:
    // match error.downcast::<std::io::Error>() {
    //     Ok(_) => println!("io::Error"),
    //     Err(e) => {
    //         // Now e is moved, can't try another downcast
    //     }
    // }
}
 
fn multiple_downcasts_owned(mut error: Error) {
    // With ownership, you'd need a loop or pattern matching
    loop {
        if let Ok(io_error) = error.downcast::<std::io::Error>() {
            println!("It's an io::Error: {}", io_error);
            break;
        }
        // error is now in Err, need to rebind
        // This pattern is awkward - downcast_ref is better for multiple checks
        break;
    }
}

For checking multiple types, downcast_ref is cleaner because it preserves the error.

Performance Considerations

use anyhow::Error;
 
fn performance() {
    // Both downcast and downcast_ref involve type checking
    // The performance difference is minimal
    
    // downcast:
    // - Checks type
    // - If match: moves inner value out, returns Ok(value)
    // - If no match: moves error back, returns Err(error)
    
    // downcast_ref:
    // - Checks type
    // - If match: returns Some(&value)
    // - If no match: returns None
    
    // The actual type check is the same
    // The difference is what happens after
    
    // For hot paths:
    // - If you always downcast to the same type: negligible difference
    // - If you check multiple types: downcast_ref is cleaner
    
    // Memory:
    // - downcast: moves inner value, deallocates Error wrapper
    // - downcast_ref: no allocation changes
}

Performance differences are minimal; the choice is primarily about ownership semantics.

The Error Message Preservation

use anyhow::{Error, anyhow};
 
fn error_context() {
    // anyhow allows adding context to errors
    
    let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
    let error: Error = Error::new(io_error).context("while reading config");
    
    // The error has context: "while reading config"
    // and cause: std::io::Error
    
    // downcast extracts just the inner error
    match error.downcast::<std::io::Error>() {
        Ok(io_error) => {
            // Context is lost, we only have the io::Error
            println!("Inner error: {}", io_error);
        }
        Err(e) => println!("Not an io::Error: {}", e),
    }
    
    // downcast_ref also gives just the inner error
    let error: Error = Error::new(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "file missing"
    )).context("while reading config");
    
    if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
        // We have the io::Error reference
        // But the context is in the outer anyhow::Error
        println!("Inner error: {}", io_error);
    }
    // The outer error still has context
}

Both methods access only the inner error; context added with .context() remains in the outer error.

Working with Custom Errors

use anyhow::{Error, Result};
use std::fmt;
 
// Custom error type
#[derive(Debug)]
enum MyError {
    InvalidInput(String),
    ProcessingFailed,
}
 
impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
            MyError::ProcessingFailed => write!(f, "Processing failed"),
        }
    }
}
 
impl std::error::Error for MyError {}
 
fn custom_error_handling() {
    let error: Error = Error::new(MyError::InvalidInput("bad data".to_string()));
    
    // downcast to get owned value
    match error.downcast::<MyError>() {
        Ok(my_error) => {
            match my_error {
                MyError::InvalidInput(msg) => println!("Invalid: {}", msg),
                MyError::ProcessingFailed => println!("Failed"),
            }
        }
        Err(e) => println!("Not MyError: {}", e),
    }
    
    // downcast_ref to inspect
    let error: Error = Error::new(MyError::ProcessingFailed);
    
    if let Some(MyError::InvalidInput(msg)) = error.downcast_ref::<MyError>() {
        println!("Invalid input: {}", msg);
    } else if let Some(MyError::ProcessingFailed) = error.downcast_ref::<MyError>() {
        println!("Processing failed");
    }
}

Both methods work with custom error types implementing std::error::Error.

Pattern Matching on Errors

use anyhow::{Error, Result};
 
fn pattern_matching(error: Error) -> Result<()> {
    // Common pattern: match on error type
    
    // Using downcast (consuming)
    match error.downcast::<std::io::Error>() {
        Ok(io_error) => {
            // Owned io::Error
            match io_error.kind() {
                std::io::ErrorKind::NotFound => {
                    // Handle not found
                    return Ok(());
                }
                kind => {
                    return Err(Error::new(std::io::Error::new(kind, io_error)));
                }
            }
        }
        Err(e) => {
            // Could try other types, but e is already moved
            // Would need to reassign
            return Err(e);
        }
    }
    
    // Using downcast_ref (non-consuming)
    fn pattern_match_ref(error: &Error) -> Result<()> {
        // Can check multiple types
        if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
            match io_error.kind() {
                std::io::ErrorKind::NotFound => {
                    return Ok(());
                }
                _ => {}
            }
        }
        
        if let Some(json_error) = error.downcast_ref::<serde_json::Error>() {
            println!("JSON error: {}", json_error);
        }
        
        Err(error.clone())
    }
}

For pattern matching on multiple types, downcast_ref enables checking without consuming.

Chain of Downcasts

use anyhow::{Error, Result};
 
fn chain_downcasts(error: Error) -> Result<()> {
    // If you need to try multiple types with downcast
    // You need to handle ownership carefully
    
    // Attempt downcast chain
    let result = error.downcast::<std::io::Error>();
    
    match result {
        Ok(io_error) => {
            // Got io::Error
            return Err(Error::new(io_error));
        }
        Err(error) => {
            // Try next type
            match error.downcast::<serde_json::Error>() {
                Ok(json_error) => {
                    return Err(Error::new(json_error));
                }
                Err(error) => {
                    // Try next type
                    // This pattern is verbose
                    Err(error)
                }
            }
        }
    }
    
    // Compare with downcast_ref (cleaner):
    fn chain_ref(error: &Error) {
        if error.downcast_ref::<std::io::Error>().is_some() {
            // Handle
        } else if error.downcast_ref::<serde_json::Error>().is_some() {
            // Handle
        }
    }
}

For checking multiple types, downcast_ref produces cleaner code.

Summary Table

fn summary() {
    // | Aspect | downcast | downcast_ref | downcast_mut |
    // |--------|----------|--------------|--------------|
    // | Self type | self (owned) | &self | &mut self |
    // | Returns | Result<T, Error> | Option<&T> | Option<&mut T> |
    // | Ownership | Moves inner on success | Borrows | Mutably borrows |
    // | On failure | Returns original in Err | Returns None | Returns None |
    // | Preserves Error | No (consumed) | Yes | Yes |
    // | Use case | Extract inner value | Inspect error | Modify inner error |
    
    // | Need | Method |
    // |------|--------|
    // | Ownership of inner value | downcast |
    // | Just inspect inner error | downcast_ref |
    // | Modify inner error | downcast_mut |
    // | Check multiple types | downcast_ref |
    // | Return inner error separately | downcast |
}

Synthesis

Quick reference:

use anyhow::Error;
 
// downcast: Takes ownership, extracts inner value
fn use_downcast(error: Error) {
    match error.downcast::<std::io::Error>() {
        Ok(io_error) => {
            // Own io_error, error is consumed
            println!("Owned: {}", io_error);
        }
        Err(original_error) => {
            // Type didn't match, got error back
            println!("Not io::Error: {}", original_error);
        }
    }
}
 
// downcast_ref: Borrows, returns reference
fn use_downcast_ref(error: &Error) {
    if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
        // Borrowed reference
        // error is still valid after
        println!("Borrowed: {}", io_error);
    }
}
 
// downcast_mut: Mutable borrow
fn use_downcast_mut(error: &mut Error) {
    if let Some(io_error) = error.downcast_mut::<std::io::Error>() {
        // Mutable reference
        // Can modify inner error
        *io_error = std::io::Error::new(std::io::ErrorKind::Other, "modified");
    }
}

Key insight: The choice between downcast and downcast_ref is fundamentally about ownership needs. Use downcast when you need to take ownership of the inner error—for example, when returning it from a function or storing it separately. Use downcast_ref when you only need to inspect the inner error—pattern matching on error types, checking error kinds, or reading error data. The downcast method enables full ownership transfer but at the cost of consuming the original Error; downcast_ref preserves the original error but only provides borrowed access. For most error handling code that needs to branch on error types, downcast_ref is the natural choice—use downcast when the inner value must be extracted and moved elsewhere.