1018-error-downcast — Error Downcast
Tutorial
The Problem
When errors are type-erased as Box<dyn Error> or Arc<dyn Error>, you lose the ability to pattern-match on specific error types. This is the trade-off of dynamic dispatch: flexibility at the cost of type information. Downcasting recovers the concrete type at runtime, using the Any mechanism under the hood.
This pattern appears wherever error types cross API boundaries: plugin systems, dynamic library interfaces, and functions returning Box<dyn Error> for flexibility. The downcast_ref and downcast methods are Rust's equivalent of Java's instanceof check plus cast.
🎯 Learning Outcomes
Box<dyn Error> erases the concrete error typedowncast_ref::<ConcreteError>() to recover a reference to the concrete typedowncast::<ConcreteError>() to take ownership of the concrete type from a BoxError::source() chain to find errors nested inside wrappers'static bounds on the error typeCode Example
#![allow(clippy::all)]
// 1018: Error Downcast
// Downcasting Box<dyn Error> to concrete type
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct DatabaseError(String);
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "database error: {}", self.0)
}
}
impl Error for DatabaseError {}
#[derive(Debug)]
struct AuthError(String);
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "auth error: {}", self.0)
}
}
impl Error for AuthError {}
#[derive(Debug)]
struct NetworkError(String);
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "network error: {}", self.0)
}
}
impl Error for NetworkError {}
// Functions returning type-erased errors
fn might_fail_db() -> Result<(), Box<dyn Error>> {
Err(Box::new(DatabaseError("timeout".into())))
}
fn might_fail_auth() -> Result<(), Box<dyn Error>> {
Err(Box::new(AuthError("expired token".into())))
}
// Approach 1: downcast_ref — borrow the concrete type
fn classify_error(err: &(dyn Error + 'static)) -> &'static str {
if err.downcast_ref::<DatabaseError>().is_some() {
"database"
} else if err.downcast_ref::<AuthError>().is_some() {
"auth"
} else if err.downcast_ref::<NetworkError>().is_some() {
"network"
} else {
"unknown"
}
}
// Approach 2: downcast — take ownership of concrete type
fn handle_error(err: Box<dyn Error>) -> String {
if let Ok(db_err) = err.downcast::<DatabaseError>() {
format!("Handling DB: {}", db_err.0)
} else {
"unhandled error".into()
}
}
// Approach 3: Type ID check
fn is_database_error(err: &(dyn Error + 'static)) -> bool {
err.downcast_ref::<DatabaseError>().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast_ref_db() {
let err: Box<dyn Error> = Box::new(DatabaseError("test".into()));
assert_eq!(classify_error(err.as_ref()), "database");
let concrete = err.downcast_ref::<DatabaseError>().unwrap();
assert_eq!(concrete.0, "test");
}
#[test]
fn test_downcast_ref_auth() {
let err: Box<dyn Error> = Box::new(AuthError("bad".into()));
assert_eq!(classify_error(err.as_ref()), "auth");
}
#[test]
fn test_downcast_ref_unknown() {
let err: Box<dyn Error> = Box::new(std::io::Error::new(std::io::ErrorKind::Other, "misc"));
assert_eq!(classify_error(err.as_ref()), "unknown");
}
#[test]
fn test_downcast_owned() {
let err: Box<dyn Error> = Box::new(DatabaseError("owned".into()));
let result = handle_error(err);
assert_eq!(result, "Handling DB: owned");
}
#[test]
fn test_downcast_owned_wrong_type() {
let err: Box<dyn Error> = Box::new(AuthError("nope".into()));
let result = handle_error(err);
assert_eq!(result, "unhandled error");
}
#[test]
fn test_is_check() {
let err: Box<dyn Error> = Box::new(DatabaseError("x".into()));
assert!(is_database_error(err.as_ref()));
let err: Box<dyn Error> = Box::new(AuthError("x".into()));
assert!(!is_database_error(err.as_ref()));
}
#[test]
fn test_from_result() {
let result = might_fail_db();
let err = result.unwrap_err();
assert!(err.downcast_ref::<DatabaseError>().is_some());
}
}Key Differences
dyn Trait; OCaml exceptions are always fully typed and matchable.downcast_ref performs a single type-ID comparison (essentially free); OCaml match compilation is similar in cost.'static bound**: Rust downcasting requires the error type to be 'static; OCaml has no equivalent restriction.None/Err; OCaml pattern matching is always exhaustive.OCaml Approach
OCaml exceptions carry typed payloads and can be matched directly without downcasting:
exception Database_error of string
exception Auth_error of string
let classify exn =
match exn with
| Database_error msg -> "database: " ^ msg
| Auth_error msg -> "auth: " ^ msg
| _ -> "unknown"
When using Or_error, the Error.to_exn and Error.of_exn functions bridge between exceptions and the Error.t type. There is no downcasting because exceptions are always typed at the match site.
Full Source
#![allow(clippy::all)]
// 1018: Error Downcast
// Downcasting Box<dyn Error> to concrete type
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct DatabaseError(String);
impl fmt::Display for DatabaseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "database error: {}", self.0)
}
}
impl Error for DatabaseError {}
#[derive(Debug)]
struct AuthError(String);
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "auth error: {}", self.0)
}
}
impl Error for AuthError {}
#[derive(Debug)]
struct NetworkError(String);
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "network error: {}", self.0)
}
}
impl Error for NetworkError {}
// Functions returning type-erased errors
fn might_fail_db() -> Result<(), Box<dyn Error>> {
Err(Box::new(DatabaseError("timeout".into())))
}
fn might_fail_auth() -> Result<(), Box<dyn Error>> {
Err(Box::new(AuthError("expired token".into())))
}
// Approach 1: downcast_ref — borrow the concrete type
fn classify_error(err: &(dyn Error + 'static)) -> &'static str {
if err.downcast_ref::<DatabaseError>().is_some() {
"database"
} else if err.downcast_ref::<AuthError>().is_some() {
"auth"
} else if err.downcast_ref::<NetworkError>().is_some() {
"network"
} else {
"unknown"
}
}
// Approach 2: downcast — take ownership of concrete type
fn handle_error(err: Box<dyn Error>) -> String {
if let Ok(db_err) = err.downcast::<DatabaseError>() {
format!("Handling DB: {}", db_err.0)
} else {
"unhandled error".into()
}
}
// Approach 3: Type ID check
fn is_database_error(err: &(dyn Error + 'static)) -> bool {
err.downcast_ref::<DatabaseError>().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast_ref_db() {
let err: Box<dyn Error> = Box::new(DatabaseError("test".into()));
assert_eq!(classify_error(err.as_ref()), "database");
let concrete = err.downcast_ref::<DatabaseError>().unwrap();
assert_eq!(concrete.0, "test");
}
#[test]
fn test_downcast_ref_auth() {
let err: Box<dyn Error> = Box::new(AuthError("bad".into()));
assert_eq!(classify_error(err.as_ref()), "auth");
}
#[test]
fn test_downcast_ref_unknown() {
let err: Box<dyn Error> = Box::new(std::io::Error::new(std::io::ErrorKind::Other, "misc"));
assert_eq!(classify_error(err.as_ref()), "unknown");
}
#[test]
fn test_downcast_owned() {
let err: Box<dyn Error> = Box::new(DatabaseError("owned".into()));
let result = handle_error(err);
assert_eq!(result, "Handling DB: owned");
}
#[test]
fn test_downcast_owned_wrong_type() {
let err: Box<dyn Error> = Box::new(AuthError("nope".into()));
let result = handle_error(err);
assert_eq!(result, "unhandled error");
}
#[test]
fn test_is_check() {
let err: Box<dyn Error> = Box::new(DatabaseError("x".into()));
assert!(is_database_error(err.as_ref()));
let err: Box<dyn Error> = Box::new(AuthError("x".into()));
assert!(!is_database_error(err.as_ref()));
}
#[test]
fn test_from_result() {
let result = might_fail_db();
let err = result.unwrap_err();
assert!(err.downcast_ref::<DatabaseError>().is_some());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast_ref_db() {
let err: Box<dyn Error> = Box::new(DatabaseError("test".into()));
assert_eq!(classify_error(err.as_ref()), "database");
let concrete = err.downcast_ref::<DatabaseError>().unwrap();
assert_eq!(concrete.0, "test");
}
#[test]
fn test_downcast_ref_auth() {
let err: Box<dyn Error> = Box::new(AuthError("bad".into()));
assert_eq!(classify_error(err.as_ref()), "auth");
}
#[test]
fn test_downcast_ref_unknown() {
let err: Box<dyn Error> = Box::new(std::io::Error::new(std::io::ErrorKind::Other, "misc"));
assert_eq!(classify_error(err.as_ref()), "unknown");
}
#[test]
fn test_downcast_owned() {
let err: Box<dyn Error> = Box::new(DatabaseError("owned".into()));
let result = handle_error(err);
assert_eq!(result, "Handling DB: owned");
}
#[test]
fn test_downcast_owned_wrong_type() {
let err: Box<dyn Error> = Box::new(AuthError("nope".into()));
let result = handle_error(err);
assert_eq!(result, "unhandled error");
}
#[test]
fn test_is_check() {
let err: Box<dyn Error> = Box::new(DatabaseError("x".into()));
assert!(is_database_error(err.as_ref()));
let err: Box<dyn Error> = Box::new(AuthError("x".into()));
assert!(!is_database_error(err.as_ref()));
}
#[test]
fn test_from_result() {
let result = might_fail_db();
let err = result.unwrap_err();
assert!(err.downcast_ref::<DatabaseError>().is_some());
}
}
Deep Comparison
Error Downcast — Comparison
Core Insight
Type erasure (Box<dyn Error>) is convenient but loses type information. Downcasting recovers it at runtime — OCaml's exception matching does this naturally, while Rust needs explicit downcasts.
OCaml Approach
type t += ...) support open matchingRust Approach
downcast_ref::<T>() — borrow as concrete type (returns Option)downcast::<T>() — take ownership (returns Result)TypeId internally (runtime reflection)Box<dyn Error> from librariesComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Type recovery | Pattern matching | downcast_ref / downcast |
| Compile-time safe | Yes (match) | No (runtime check) |
| Cost | Zero | TypeId comparison |
| Ownership | N/A | downcast consumes Box |
| Preferred approach | Exceptions / variants | Typed enum (avoid downcast) |
Exercises
Error::source() chain recursively and returns a Vec<&str> of all error messages from root to leaf.try_recover<E: Error + 'static>(err: Box<dyn Error>) -> Result<(), E> generic function that downcasts and returns the specific error type if it matches.WrappedError struct that wraps another Box<dyn Error> and implements Error::source(). Show that downcasting into the inner error still works via source-chain walking.