What are the trade-offs between anyhow::Error::downcast and downcast_ref for runtime type checking?
downcast returns an owned T from the error by consuming the Error, while downcast_ref returns a reference &T without consuming the Error. This fundamental difference means downcast is useful when you need to extract and use the concrete error type, while downcast_ref is useful for inspecting or matching on error types without taking ownershipâcritical when you need to continue using the error for other purposes like chaining, logging, or further downcasts.
The anyhow Error Type
use anyhow::Error;
use std::fmt;
// anyhow::Error can hold any error that implements:
// - std::error::Error
// - Send + Sync + 'static
fn basic_error() -> Result<(), Error> {
// anyhow::Error wraps concrete error types
Err(Error::msg("something went wrong"))
}
// You can downcast to retrieve the concrete type
fn concrete_error() -> Result<(), Error> {
Err(Error::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
)))
}anyhow::Error is a type-erased error container that can hold any error type.
The downcast_ref Method
use anyhow::Error;
fn downcast_ref_example() {
let error: Error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
).into();
// downcast_ref returns Option<&T>
// It borrows the Error, doesn't consume it
if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
println!("IO error: {}", io_error);
println!("Kind: {:?}", io_error.kind());
}
// error is still available after downcast_ref
println!("Full error: {}", error);
// You can downcast multiple times
if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
println!("Still accessible: {}", io_error);
}
}downcast_ref borrows the error and returns a reference to the inner type if it matches.
The downcast Method
use anyhow::Error;
fn downcast_example() {
let error: Error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
).into();
// downcast returns Result<T, Error>
// It CONSUMES the Error
match error.downcast::<std::io::Error>() {
Ok(io_error) => {
// We own the io::Error now
println!("IO error: {}", io_error);
println!("Kind: {:?}", io_error.kind());
// error is no longer accessible
}
Err(original_error) => {
// Type didn't match
// original_error is returned so you don't lose it
println!("Not an IO error: {}", original_error);
}
}
// After downcast, 'error' is moved/consumed
// Cannot use it here
}downcast consumes the error and returns either the inner type or the original error on mismatch.
Key Difference: Ownership
use anyhow::Error;
fn ownership_comparison() {
let error: Error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
).into();
// downcast_ref: Borrows, doesn't consume
if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
println!("Got reference: {}", io_error);
}
// error is still valid here
println!("Error still exists: {}", error);
// downcast: Consumes
let result = error.downcast::<std::io::Error>();
// error is MOVED here, no longer accessible
match result {
Ok(io_error) => {
// We OWN the std::io::Error now
// Can return it, modify it, etc.
}
Err(original_error) => {
// We still own the anyhow::Error
// Type didn't match
}
}
}The core trade-off: downcast_ref preserves ownership, downcast transfers it.
Use Case: Inspecting Without Consuming
use anyhow::Error;
fn inspect_error(error: &Error) {
// When you need to inspect but keep the error:
// Log based on error type
if error.downcast_ref::<std::io::Error>().is_some() {
println!("IO error occurred");
} else if error.downcast_ref::<reqwest::Error>().is_some() {
println!("HTTP error occurred");
}
// Pattern match on multiple types
if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
match io_err.kind() {
std::io::ErrorKind::NotFound => println!("File not found"),
std::io::ErrorKind::PermissionDenied => println!("Permission denied"),
_ => println!("Other IO error"),
}
}
// error is still usable after inspection
}
fn caller() {
let error: Error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
).into();
inspect_error(&error);
// Can still use error after inspection
println!("Original error: {}", error);
}Use downcast_ref when you need to inspect errors without consuming them.
Use Case: Extracting and Returning
use anyhow::Error;
// When you need to convert anyhow::Error back to a concrete type
fn convert_to_concrete(error: Error) -> Result<String, std::io::Error> {
// downcast to get owned std::io::Error
match error.downcast::<std::io::Error>() {
Ok(io_error) => {
// We own the io::Error, can return it
Err(io_error)
}
Err(other_error) => {
// Not an io::Error
// other_error is the original anyhow::Error
// Convert it to string
Ok(format!("Other error: {}", other_error))
}
}
}
// This pattern is useful when:
// - Interfacing with code that expects concrete error types
// - Converting from anyhow back to specific errorsUse downcast when you need owned error types for further processing or return.
Use Case: Multiple Downcast Attempts
use anyhow::Error;
fn multiple_downcasts(error: &Error) {
// downcast_ref allows multiple attempts
if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
println!("IO: {}", io_err);
} else if let Some(http_err) = error.downcast_ref::<reqwest::Error>() {
println!("HTTP: {}", http_err);
} else if let Some(json_err) = error.downcast_ref::<serde_json::Error>() {
println!("JSON: {}", json_err);
} else {
println!("Unknown error type");
}
// With downcast(), you'd have to handle ownership:
// error.downcast::<std::io::Error>()
// .map(|e| ...)
// .or_else(|e| e.downcast::<reqwest::Error>())
// .or_else(|e| e.downcast::<serde_json::Error>())
// Much more awkward!
}downcast_ref is cleaner when checking multiple error types.
Use Case: Error Chaining
use anyhow::{Error, Context};
fn with_context() -> Result<(), Error> {
std::fs::read_to_string("config.txt")
.context("Failed to read config")?;
Ok(())
}
fn handle_error() {
match with_context() {
Ok(_) => println!("Success"),
Err(error) => {
// With downcast_ref, we can inspect the chain
// and still use the full error
if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
println!("Root cause is IO: {}", io_err);
}
// Still have full error chain for display
for cause in error.chain() {
println!("Cause: {}", cause);
}
// Still have full context
println!("Full error: {:?}", error);
}
}
}downcast_ref preserves the error chain and context for further use.
Performance Considerations
use anyhow::Error;
fn performance_comparison() {
let error: Error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
).into();
// downcast_ref: Borrow, check type, return reference
// - No allocation
// - No cloning
// - Just type check and pointer cast
if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
// Fast: just a type check and reference
}
// downcast: Move, type check, return inner or original
// - No allocation on success
// - On failure: returns original Error (moved)
let result = error.downcast::<std::io::Error>();
// Both are O(1) - just type checking
// The difference is ownership semantics, not performance
}Both methods have similar performance; the difference is ownership, not speed.
Error Type Not Matching
use anyhow::Error;
fn type_mismatch() {
let error: Error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found"
).into();
// downcast_ref: Returns None if type doesn't match
if let Some(parse_err) = error.downcast_ref::<serde_json::Error>() {
// Not reached - it's an io::Error, not json::Error
} else {
println!("Not a JSON error");
}
// downcast: Returns Err(original_error) if type doesn't match
let result = error.downcast::<serde_json::Error>();
match result {
Ok(_parse_err) => {
// Won't reach here
}
Err(original_error) => {
// We get the original error back
println!("Not a JSON error: {}", original_error);
// original_error is still usable
}
}
}Both methods safely handle type mismatches; downcast_ref returns None, downcast returns Err.
Working with Custom Errors
use anyhow::Error;
use std::fmt;
#[derive(Debug)]
enum MyError {
NetworkError(String),
ValidationError(String),
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MyError::NetworkError(msg) => write!(f, "Network: {}", msg),
MyError::ValidationError(msg) => write!(f, "Validation: {}", msg),
}
}
}
impl std::error::Error for MyError {}
fn custom_error_handling() {
let error: Error = MyError::NetworkError("Connection timeout".into()).into();
// downcast_ref to inspect
if let Some(my_err) = error.downcast_ref::<MyError>() {
match my_err {
MyError::NetworkError(msg) => println!("Network issue: {}", msg),
MyError::ValidationError(msg) => println!("Validation issue: {}", msg),
}
}
// downcast to extract
let error: Error = MyError::ValidationError("Invalid input".into()).into();
match error.downcast::<MyError>() {
Ok(MyError::ValidationError(msg)) => {
println!("Validation failed: {}", msg);
// We own the error data now
}
Ok(MyError::NetworkError(msg)) => {
println!("Network failed: {}", msg);
}
Err(other) => {
println!("Different error type: {}", other);
}
}
}Both methods work with custom error types implementing std::error::Error.
Downcasting in Error Handlers
use anyhow::Error;
fn error_handler(error: &Error) {
// Pattern: Try multiple known error types
// Check for specific errors we know how to handle
if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
match io_err.kind() {
std::io::ErrorKind::NotFound => {
println!("File not found - check path");
}
std::io::ErrorKind::PermissionDenied => {
println!("Permission denied - check permissions");
}
_ => {
println!("IO error: {}", io_err);
}
}
} else if let Some(http_err) = error.downcast_ref::<reqwest::Error>() {
if http_err.is_timeout() {
println!("Request timed out - retry");
} else {
println!("HTTP error: {}", http_err);
}
} else {
// Unknown error type
println!("Unknown error: {}", error);
}
}downcast_ref enables error-type-specific handling without consuming the error.
Downcasting for Error Conversion
use anyhow::Error;
// Convert anyhow::Error to specific error types for library boundaries
fn convert_to_io_error(error: Error) -> std::io::Error {
match error.downcast::<std::io::Error>() {
Ok(io_error) => io_error,
Err(other_error) => {
// Not an io::Error, wrap it
std::io::Error::new(
std::io::ErrorKind::Other,
other_error.to_string()
)
}
}
}
// This pattern is useful when:
// - Library returns anyhow::Error internally
// - Public API must return specific error type
fn library_function() -> Result<String, std::io::Error> {
internal_function().map_err(|e| convert_to_io_error(e))
}
fn internal_function() -> Result<String, Error> {
// May return various error types
Ok("result".to_string())
}downcast is essential when converting from anyhow::Error to concrete error types.
Method Signatures
use anyhow::Error;
// Method signatures:
impl Error {
// downcast_ref: Returns Option<&T>
pub fn downcast_ref<T: std::error::Error + Send + Sync + 'static>(&self)
-> Option<&T>
{
// Borrow self, return reference to inner type
}
// downcast: Returns Result<T, Error>
pub fn downcast<T: std::error::Error + Send + Sync + 'static>(self)
-> Result<T, Error>
{
// Consume self, return owned inner type or original error
}
}The signatures encode the ownership semantics: &self vs self.
Synthesis
Comparison table:
| Aspect | downcast_ref |
downcast |
|---|---|---|
| Ownership | Borrows Error |
Consumes Error |
| Return type | Option<&T> |
Result<T, Error> |
| Error on mismatch | None |
Err(original_error) |
| Can use error after? | Yes | No (moved) |
| Multiple downcasts | Straightforward | Awkward (must chain) |
| Use case | Inspection, matching | Extraction, conversion |
When to use downcast_ref:
// Inspecting errors without consuming
if let Some(io_err) = error.downcast_ref::<std::io::Error>() {
// Handle based on error kind
}
// Multiple type checks
if error.downcast_ref::<std::io::Error>().is_some() {
// Handle IO
} else if error.downcast_ref::<reqwest::Error>().is_some() {
// Handle HTTP
}
// Error logging/reporting (preserves full error)
log_error(&error); // Can still use error afterWhen to use downcast:
// Converting to concrete error type
match error.downcast::<std::io::Error>() {
Ok(io_err) => return Err(io_err), // Return concrete type
Err(other) => { /* handle other */ }
}
// Extracting and processing owned error
let io_error = error.downcast::<std::io::Error>()?;
// Own the std::io::Error, can modify or return it
// Library boundary conversion
fn to_concrete(error: Error) -> Result<Data, std::io::Error> {
// Must return std::io::Error, not anyhow::Error
match error.downcast() {
Ok(io_err) => Err(io_err),
Err(other) => Err(std::io::Error::new(Other, other)),
}
}Key insight: The trade-off between downcast and downcast_ref is purely about ownership, not functionality or performance. downcast_ref is the right choice when you're inspecting errors for handling, logging, or conditional logicâthe error remains usable after inspection. downcast is the right choice when you need to extract the concrete error type for return, further processing, or when converting from anyhow::Error to a specific error type for library boundaries. The downcast method's Result<T, Error> return type is specifically designed to preserve the original error on mismatch, so you don't lose information when the type doesn't match. Use downcast_ref by default; use downcast when you specifically need ownership of the inner error.
