ExamplesBy LevelBy TopicLearning Paths
312 Intermediate

312: Error Downcasting

Functional Programming

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

  • • Use error.downcast_ref::<ConcreteType>() to attempt runtime type recovery
  • • Match on Some(concrete) vs None to handle specific vs unknown error types
  • • Understand the 'static lifetime requirement for downcastable error types
  • • Walk the source() chain to downcast errors at any level
  • Code 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

  • Static vs dynamic: OCaml's variant matching is static and compile-time checked; Rust's downcast_ref is a dynamic runtime check.
  • When needed: Downcasting is necessary when errors are stored as dyn Error; with concrete error enum types, pattern matching suffices in Rust too.
  • Source chain: Rust can downcast errors anywhere in the source() chain; OCaml exceptions must be explicitly carried through the call stack.
  • Performance: 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());
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Write a function that takes a Box<dyn Error> and attempts to downcast it to three different concrete types, logging a type-specific message for each.
  • Walk the full source() chain of a nested error, attempting to downcast each level, and return the first level that is a specific IoError.
  • Demonstrate that a dyn Error without 'static cannot be downcast, and explain why the 'static bound is required.
  • Open Source Repos