ExamplesBy LevelBy TopicLearning Paths
1006 Intermediate

1006 — Multiple Error Types

Functional Programming

Tutorial

The Problem

Handle functions that return different error types in the same call chain. Compare two approaches: Box<dyn Error> (flexible, type-erased) and a typed AppError enum (exhaustive, structured). Implement From conversions for the enum approach to enable ? operator chaining. Compare with OCaml's unified variant and polymorphic variants.

🎯 Learning Outcomes

  • • Use Box<dyn std::error::Error> as a universal error type that accepts any Error implementor
  • • Understand that ? on ParseIntError in a -> Result<T, Box<dyn Error>> function auto-boxes via From
  • • Build a typed AppError enum with From impls for each sub-error type
  • • Compare the trade-offs: Box<dyn Error> (flexible/simple) vs enum (exhaustive/structured)
  • • Map Rust's approach to OCaml's unified variant enum and polymorphic variants
  • • Choose the right approach: Box<dyn Error> for applications, typed enum for libraries
  • Code Example

    #![allow(clippy::all)]
    // 1006: Multiple Error Types
    // Unifying multiple error types: Box<dyn Error> vs enum approach
    
    use std::fmt;
    use std::num::ParseIntError;
    
    // Individual error types
    #[derive(Debug)]
    struct IoError(String);
    
    impl fmt::Display for IoError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "IO error: {}", self.0)
        }
    }
    impl std::error::Error for IoError {}
    
    #[derive(Debug)]
    struct NetError(String);
    
    impl fmt::Display for NetError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "network error: {}", self.0)
        }
    }
    impl std::error::Error for NetError {}
    
    // Approach 1: Box<dyn Error> — quick and flexible
    fn do_io_boxed() -> Result<String, Box<dyn std::error::Error>> {
        Err(Box::new(IoError("file not found".into())))
    }
    
    fn do_parse_boxed(s: &str) -> Result<i64, Box<dyn std::error::Error>> {
        let n: i64 = s.parse()?; // ParseIntError auto-boxed
        Ok(n)
    }
    
    fn do_net_boxed() -> Result<String, Box<dyn std::error::Error>> {
        Err(Box::new(NetError("timeout".into())))
    }
    
    fn process_boxed() -> Result<i64, Box<dyn std::error::Error>> {
        let data = do_io_boxed().or_else(|_| Ok::<_, Box<dyn std::error::Error>>("42".into()))?;
        let parsed = do_parse_boxed(&data)?;
        let _response =
            do_net_boxed().or_else(|_| Ok::<String, Box<dyn std::error::Error>>("ok".into()))?;
        Ok(parsed)
    }
    
    // Approach 2: Typed enum — exhaustive matching
    #[derive(Debug)]
    enum AppError {
        Io(IoError),
        Parse(ParseIntError),
        Net(NetError),
    }
    
    impl fmt::Display for AppError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                AppError::Io(e) => write!(f, "{}", e),
                AppError::Parse(e) => write!(f, "parse: {}", e),
                AppError::Net(e) => write!(f, "{}", e),
            }
        }
    }
    impl std::error::Error for AppError {}
    
    impl From<IoError> for AppError {
        fn from(e: IoError) -> Self {
            AppError::Io(e)
        }
    }
    impl From<ParseIntError> for AppError {
        fn from(e: ParseIntError) -> Self {
            AppError::Parse(e)
        }
    }
    impl From<NetError> for AppError {
        fn from(e: NetError) -> Self {
            AppError::Net(e)
        }
    }
    
    fn do_io_typed() -> Result<String, IoError> {
        Ok("42".into())
    }
    
    fn do_parse_typed(s: &str) -> Result<i64, ParseIntError> {
        s.parse()
    }
    
    fn process_typed() -> Result<i64, AppError> {
        let data = do_io_typed()?; // IoError -> AppError
        let parsed = do_parse_typed(&data)?; // ParseIntError -> AppError
        Ok(parsed)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_boxed_error() {
            let result = process_boxed();
            assert_eq!(result.unwrap(), 42);
        }
    
        #[test]
        fn test_boxed_io_error() {
            let err = do_io_boxed().unwrap_err();
            assert!(err.to_string().contains("IO error"));
        }
    
        #[test]
        fn test_typed_success() {
            assert_eq!(process_typed().unwrap(), 42);
        }
    
        #[test]
        fn test_typed_pattern_match() {
            let err: AppError = IoError("test".into()).into();
            assert!(matches!(err, AppError::Io(_)));
    
            let err: AppError = "abc".parse::<i64>().unwrap_err().into();
            assert!(matches!(err, AppError::Parse(_)));
        }
    
        #[test]
        fn test_boxed_parse_error() {
            let result = do_parse_boxed("not_a_number");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_display_format() {
            let err = AppError::Net(NetError("timeout".into()));
            assert_eq!(err.to_string(), "network error: timeout");
        }
    }

    Key Differences

    AspectRust Box<dyn Error>Rust typed enumOCaml unified variantOCaml poly variants
    ExhaustivenessNoYesYesNo
    ConversionAuto (blanket From)Explicit From implsManual wrappingStructural subtyping
    Pattern matchDowncast neededDirect matchDirect matchFlexible
    Library useNot recommendedRecommendedRecommendedPossible
    VerbosityLowMediumLowLow

    The general rule: use typed enums for library crates (callers need to match on errors), use Box<dyn Error> or anyhow::Error for application code where errors are logged rather than matched.

    OCaml Approach

    OCaml's standard approach is a unified app_error variant: type app_error = Io of io_error | Parse of parse_error | Net of net_error. Functions explicitly wrap errors: Result.map_error (fun e -> Io e). Polymorphic variants ([> \FileNotFound \| \ReadError of string]) provide open, extensible error types without a central enum — more flexible but harder to reason about exhaustively.

    Full Source

    #![allow(clippy::all)]
    // 1006: Multiple Error Types
    // Unifying multiple error types: Box<dyn Error> vs enum approach
    
    use std::fmt;
    use std::num::ParseIntError;
    
    // Individual error types
    #[derive(Debug)]
    struct IoError(String);
    
    impl fmt::Display for IoError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "IO error: {}", self.0)
        }
    }
    impl std::error::Error for IoError {}
    
    #[derive(Debug)]
    struct NetError(String);
    
    impl fmt::Display for NetError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "network error: {}", self.0)
        }
    }
    impl std::error::Error for NetError {}
    
    // Approach 1: Box<dyn Error> — quick and flexible
    fn do_io_boxed() -> Result<String, Box<dyn std::error::Error>> {
        Err(Box::new(IoError("file not found".into())))
    }
    
    fn do_parse_boxed(s: &str) -> Result<i64, Box<dyn std::error::Error>> {
        let n: i64 = s.parse()?; // ParseIntError auto-boxed
        Ok(n)
    }
    
    fn do_net_boxed() -> Result<String, Box<dyn std::error::Error>> {
        Err(Box::new(NetError("timeout".into())))
    }
    
    fn process_boxed() -> Result<i64, Box<dyn std::error::Error>> {
        let data = do_io_boxed().or_else(|_| Ok::<_, Box<dyn std::error::Error>>("42".into()))?;
        let parsed = do_parse_boxed(&data)?;
        let _response =
            do_net_boxed().or_else(|_| Ok::<String, Box<dyn std::error::Error>>("ok".into()))?;
        Ok(parsed)
    }
    
    // Approach 2: Typed enum — exhaustive matching
    #[derive(Debug)]
    enum AppError {
        Io(IoError),
        Parse(ParseIntError),
        Net(NetError),
    }
    
    impl fmt::Display for AppError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                AppError::Io(e) => write!(f, "{}", e),
                AppError::Parse(e) => write!(f, "parse: {}", e),
                AppError::Net(e) => write!(f, "{}", e),
            }
        }
    }
    impl std::error::Error for AppError {}
    
    impl From<IoError> for AppError {
        fn from(e: IoError) -> Self {
            AppError::Io(e)
        }
    }
    impl From<ParseIntError> for AppError {
        fn from(e: ParseIntError) -> Self {
            AppError::Parse(e)
        }
    }
    impl From<NetError> for AppError {
        fn from(e: NetError) -> Self {
            AppError::Net(e)
        }
    }
    
    fn do_io_typed() -> Result<String, IoError> {
        Ok("42".into())
    }
    
    fn do_parse_typed(s: &str) -> Result<i64, ParseIntError> {
        s.parse()
    }
    
    fn process_typed() -> Result<i64, AppError> {
        let data = do_io_typed()?; // IoError -> AppError
        let parsed = do_parse_typed(&data)?; // ParseIntError -> AppError
        Ok(parsed)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_boxed_error() {
            let result = process_boxed();
            assert_eq!(result.unwrap(), 42);
        }
    
        #[test]
        fn test_boxed_io_error() {
            let err = do_io_boxed().unwrap_err();
            assert!(err.to_string().contains("IO error"));
        }
    
        #[test]
        fn test_typed_success() {
            assert_eq!(process_typed().unwrap(), 42);
        }
    
        #[test]
        fn test_typed_pattern_match() {
            let err: AppError = IoError("test".into()).into();
            assert!(matches!(err, AppError::Io(_)));
    
            let err: AppError = "abc".parse::<i64>().unwrap_err().into();
            assert!(matches!(err, AppError::Parse(_)));
        }
    
        #[test]
        fn test_boxed_parse_error() {
            let result = do_parse_boxed("not_a_number");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_display_format() {
            let err = AppError::Net(NetError("timeout".into()));
            assert_eq!(err.to_string(), "network error: timeout");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_boxed_error() {
            let result = process_boxed();
            assert_eq!(result.unwrap(), 42);
        }
    
        #[test]
        fn test_boxed_io_error() {
            let err = do_io_boxed().unwrap_err();
            assert!(err.to_string().contains("IO error"));
        }
    
        #[test]
        fn test_typed_success() {
            assert_eq!(process_typed().unwrap(), 42);
        }
    
        #[test]
        fn test_typed_pattern_match() {
            let err: AppError = IoError("test".into()).into();
            assert!(matches!(err, AppError::Io(_)));
    
            let err: AppError = "abc".parse::<i64>().unwrap_err().into();
            assert!(matches!(err, AppError::Parse(_)));
        }
    
        #[test]
        fn test_boxed_parse_error() {
            let result = do_parse_boxed("not_a_number");
            assert!(result.is_err());
        }
    
        #[test]
        fn test_display_format() {
            let err = AppError::Net(NetError("timeout".into()));
            assert_eq!(err.to_string(), "network error: timeout");
        }
    }

    Deep Comparison

    Multiple Error Types — Comparison

    Core Insight

    When functions call code with different error types, you need a unification strategy. Rust offers two: type-erased (Box<dyn Error>) and typed (enum with From impls).

    OCaml Approach

  • • Unified variant types manually wrap each sub-error
  • • Polymorphic variants auto-unify but lose exhaustiveness guarantees
  • • No standard trait object equivalent to Box<dyn Error>
  • Rust Approach

  • Box<dyn Error>: any error type auto-converts, but you lose pattern matching
  • • Typed enum + From impls: more boilerplate, full pattern matching retained
  • • The ? operator works with both approaches
  • Comparison Table

    AspectOCaml VariantOCaml Poly VariantRust Box<dyn>Rust Enum
    Setup costMediumLowLowMedium
    Pattern matchingYesPartialNo (need downcast)Yes, exhaustive
    ExtensibilityClosedOpenOpenClosed
    PerformanceZero-costZero-costHeap allocationZero-cost
    Best forLibrariesPrototypingScripts/prototypesLibraries/apps

    Exercises

  • Use anyhow::anyhow!("message") and anyhow::Context::context to rewrite process_boxed without defining any error types.
  • Add a fourth AppError::Config(String) variant and implement its From conversion.
  • Write fn collect_errors(results: Vec<Result<i32, AppError>>) -> (Vec<i32>, Vec<AppError>) that separates successes and errors.
  • Implement fmt::Display for AppError using .source() to print the full chain.
  • In OCaml, extend app_error with a WithContext { context: string; inner: app_error } variant and write a display_chain function that prints the full error hierarchy.
  • Open Source Repos