312: Error Downcasting
Tutorial Video
Text description (accessibility)
This video demonstrates the "312: Error Downcasting" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. When errors are stored as `Box<dyn Error>` for flexibility, the concrete type is erased. Key difference from OCaml: 1. **Static vs dynamic**: OCaml's variant matching is static and compile
Tutorial
The Problem
When errors are stored as Box<dyn Error> for flexibility, the concrete type is erased. Downcasting recovers the concrete type at runtime when specific error handling is needed — retrying on network timeouts but propagating authentication errors, for example. This is downcast_ref::<ConcreteType>() on a dyn Error — the Rust equivalent of instanceof checks or catch (SpecificException e) in Java/Python.
🎯 Learning Outcomes
error.downcast_ref::<ConcreteType>() to attempt runtime type recoverySome(concrete) vs None to handle specific vs unknown error types'static lifetime requirement for downcastable error typessource() chain to downcast errors at any levelCode Example
#![allow(clippy::all)]
//! # Downcasting Boxed Errors
//!
//! `downcast_ref::<T>()` recovers the concrete type from `Box<dyn Error>`.
use std::error::Error;
use std::fmt;
#[derive(Debug, PartialEq)]
pub struct ParseError {
pub input: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "parse error: '{}'", self.input)
}
}
impl Error for ParseError {}
#[derive(Debug)]
pub struct NetworkError {
pub code: u32,
pub message: String,
}
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "network error {}: {}", self.code, self.message)
}
}
impl Error for NetworkError {}
/// Handle error by downcasting to specific types
pub fn handle_error(e: &(dyn Error + 'static)) -> String {
if let Some(pe) = e.downcast_ref::<ParseError>() {
return format!("Parse error for: {}", pe.input);
}
if let Some(ne) = e.downcast_ref::<NetworkError>() {
return format!("Network {}: {}", ne.code, ne.message);
}
format!("Unknown: {}", e)
}
/// Create heterogeneous error collection
pub fn make_errors() -> Vec<Box<dyn Error>> {
vec![
Box::new(ParseError {
input: "abc".to_string(),
}),
Box::new(NetworkError {
code: 404,
message: "not found".to_string(),
}),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast_ref_success() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "x".to_string(),
});
assert!(e.downcast_ref::<ParseError>().is_some());
assert!(e.downcast_ref::<NetworkError>().is_none());
}
#[test]
fn test_handle_parse_error() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "test".to_string(),
});
let result = handle_error(e.as_ref());
assert!(result.contains("Parse error"));
}
#[test]
fn test_handle_network_error() {
let e: Box<dyn Error> = Box::new(NetworkError {
code: 500,
message: "fail".to_string(),
});
let result = handle_error(e.as_ref());
assert!(result.contains("Network 500"));
}
#[test]
fn test_downcast_box() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "abc".to_string(),
});
let result = e.downcast::<ParseError>();
assert!(result.is_ok());
}
}Key Differences
downcast_ref is a dynamic runtime check.dyn Error; with concrete error enum types, pattern matching suffices in Rust too.source() chain; OCaml exceptions must be explicitly carried through the call stack.downcast_ref uses type IDs for O(1) checking; multiple downcasts are faster than multiple pattern matches but require knowing all possible types.OCaml Approach
OCaml's exception system uses match exn with | SpecificException data -> ... for typed error discrimination. For result error values, pattern matching on variant types achieves the same without runtime type checks:
let handle_error = function
| `Parse input -> Printf.printf "Parse error: '%s'\n" input
| `Network (code, msg) -> Printf.printf "Network %d: %s\n" code msg
| e -> Printf.printf "Unknown: %s\n" (to_string e)
OCaml's pattern matching on sum types is static and exhaustive — downcasting is unnecessary when using algebraic types.
Full Source
#![allow(clippy::all)]
//! # Downcasting Boxed Errors
//!
//! `downcast_ref::<T>()` recovers the concrete type from `Box<dyn Error>`.
use std::error::Error;
use std::fmt;
#[derive(Debug, PartialEq)]
pub struct ParseError {
pub input: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "parse error: '{}'", self.input)
}
}
impl Error for ParseError {}
#[derive(Debug)]
pub struct NetworkError {
pub code: u32,
pub message: String,
}
impl fmt::Display for NetworkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "network error {}: {}", self.code, self.message)
}
}
impl Error for NetworkError {}
/// Handle error by downcasting to specific types
pub fn handle_error(e: &(dyn Error + 'static)) -> String {
if let Some(pe) = e.downcast_ref::<ParseError>() {
return format!("Parse error for: {}", pe.input);
}
if let Some(ne) = e.downcast_ref::<NetworkError>() {
return format!("Network {}: {}", ne.code, ne.message);
}
format!("Unknown: {}", e)
}
/// Create heterogeneous error collection
pub fn make_errors() -> Vec<Box<dyn Error>> {
vec![
Box::new(ParseError {
input: "abc".to_string(),
}),
Box::new(NetworkError {
code: 404,
message: "not found".to_string(),
}),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast_ref_success() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "x".to_string(),
});
assert!(e.downcast_ref::<ParseError>().is_some());
assert!(e.downcast_ref::<NetworkError>().is_none());
}
#[test]
fn test_handle_parse_error() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "test".to_string(),
});
let result = handle_error(e.as_ref());
assert!(result.contains("Parse error"));
}
#[test]
fn test_handle_network_error() {
let e: Box<dyn Error> = Box::new(NetworkError {
code: 500,
message: "fail".to_string(),
});
let result = handle_error(e.as_ref());
assert!(result.contains("Network 500"));
}
#[test]
fn test_downcast_box() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "abc".to_string(),
});
let result = e.downcast::<ParseError>();
assert!(result.is_ok());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_downcast_ref_success() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "x".to_string(),
});
assert!(e.downcast_ref::<ParseError>().is_some());
assert!(e.downcast_ref::<NetworkError>().is_none());
}
#[test]
fn test_handle_parse_error() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "test".to_string(),
});
let result = handle_error(e.as_ref());
assert!(result.contains("Parse error"));
}
#[test]
fn test_handle_network_error() {
let e: Box<dyn Error> = Box::new(NetworkError {
code: 500,
message: "fail".to_string(),
});
let result = handle_error(e.as_ref());
assert!(result.contains("Network 500"));
}
#[test]
fn test_downcast_box() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "abc".to_string(),
});
let result = e.downcast::<ParseError>();
assert!(result.is_ok());
}
}
Deep Comparison
error-downcasting
See README.md for details.
Exercises
Box<dyn Error> and attempts to downcast it to three different concrete types, logging a type-specific message for each.source() chain of a nested error, attempting to downcast each level, and return the first level that is a specific IoError.dyn Error without 'static cannot be downcast, and explain why the 'static bound is required.