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.
