ExamplesBy LevelBy TopicLearning Paths
295 Intermediate

295: Implementing std::error::Error

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "295: Implementing std::error::Error" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The `std::error::Error` trait is the common interface for all Rust errors, enabling error chaining, dynamic dispatch, and interoperability between libraries. Key difference from OCaml: 1. **Standard interface**: Rust's `std::error::Error` is the universal error contract; OCaml has no equivalent standard trait.

Tutorial

The Problem

The std::error::Error trait is the common interface for all Rust errors, enabling error chaining, dynamic dispatch, and interoperability between libraries. Implementing it properly — with Display for user messages, Debug for developer output, and source() for causal chains — is the foundation of production-quality error handling. This mirrors the interface that anyhow, thiserror, and the broader ecosystem expect.

🎯 Learning Outcomes

  • • Implement the full std::error::Error trait: Display, Debug, and optionally source()
  • • Use source() to create linked error chains that expose root causes
  • • Understand the Box<dyn Error + Send + Sync> pattern for type-erased error storage
  • • Recognize Send + Sync bounds as requirements for using errors across thread boundaries
  • Code Example

    use std::error::Error;
    use std::fmt;
    
    #[derive(Debug)]
    struct ParseError { input: String, reason: String }
    
    impl fmt::Display for ParseError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "failed to parse '{}': {}", self.input, self.reason)
        }
    }
    
    impl Error for ParseError {}  // source() defaults to None

    Key Differences

  • Standard interface: Rust's std::error::Error is the universal error contract; OCaml has no equivalent standard trait.
  • Chaining: Rust's source() creates a traversable linked list of causes; OCaml requires manual nested error structures.
  • Thread safety: Box<dyn Error + Send + Sync> enables sending errors across thread boundaries; OCaml's GC handles this transparently.
  • Ecosystem: ? operator, Box<dyn Error>, anyhow, and thiserror all depend on std::error::Error as the common interface.
  • OCaml Approach

    OCaml does not have a standard error interface — errors are plain values. The idiomatic approach in modern OCaml uses Result.t with a custom error type and provides to_string for display. Libraries like Fmt provide pp_error conventions:

    type error = { field: string; cause: string }
    
    let string_of_error { field; cause } =
      Printf.sprintf "field '%s' invalid: %s" field cause
    

    OCaml lacks a standard "error chaining" mechanism — nested error types or exception causes must be manually threaded.

    Full Source

    #![allow(clippy::all)]
    //! # Implementing std::error::Error
    //!
    //! `std::error::Error` requires Display + Debug and optionally provides `source()`.
    
    use std::error::Error;
    use std::fmt;
    
    /// Low-level parse error
    #[derive(Debug)]
    pub struct ParseError {
        pub input: String,
        pub reason: String,
    }
    
    impl fmt::Display for ParseError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "failed to parse '{}': {}", self.input, self.reason)
        }
    }
    
    impl Error for ParseError {} // source() defaults to None
    
    /// Higher-level validation error that wraps a cause
    #[derive(Debug)]
    pub struct ValidationError {
        pub field: String,
        pub source: Box<dyn Error + Send + Sync>,
    }
    
    impl fmt::Display for ValidationError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "validation failed for field '{}'", self.field)
        }
    }
    
    impl Error for ValidationError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(self.source.as_ref())
        }
    }
    
    /// Parse a string as an age (u8)
    pub fn parse_age(s: &str) -> Result<u8, ParseError> {
        s.parse::<u8>().map_err(|e| ParseError {
            input: s.to_string(),
            reason: e.to_string(),
        })
    }
    
    /// Validate user age with context
    pub fn validate_user_age(s: &str) -> Result<u8, ValidationError> {
        parse_age(s).map_err(|e| ValidationError {
            field: "age".to_string(),
            source: Box::new(e),
        })
    }
    
    /// Print full error chain
    pub fn print_error_chain(e: &dyn Error) -> String {
        let mut result = format!("Error: {}", e);
        let mut cause = e.source();
        while let Some(c) = cause {
            result.push_str(&format!("\n  Caused by: {}", c));
            cause = c.source();
        }
        result
    }
    
    /// Collect heterogeneous errors
    pub fn collect_errors() -> Vec<Box<dyn Error>> {
        vec![Box::new(ParseError {
            input: "x".to_string(),
            reason: "not a number".to_string(),
        })]
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_error_display() {
            let e = ParseError {
                input: "abc".to_string(),
                reason: "invalid".to_string(),
            };
            let msg = format!("{}", e);
            assert!(msg.contains("abc"));
        }
    
        #[test]
        fn test_parse_error_is_error_trait() {
            let e: Box<dyn Error> = Box::new(ParseError {
                input: "x".to_string(),
                reason: "bad".to_string(),
            });
            assert!(e.source().is_none());
        }
    
        #[test]
        fn test_validation_error_source() {
            let result = validate_user_age("abc");
            assert!(result.is_err());
            let e = result.unwrap_err();
            assert!(e.source().is_some());
        }
    
        #[test]
        fn test_valid_age() {
            assert_eq!(validate_user_age("25").unwrap(), 25);
        }
    
        #[test]
        fn test_error_chain_string() {
            let result = validate_user_age("bad");
            if let Err(e) = result {
                let chain = print_error_chain(&e);
                assert!(chain.contains("validation failed"));
                assert!(chain.contains("Caused by"));
            }
        }
    
        #[test]
        fn test_collect_heterogeneous() {
            let errors = collect_errors();
            assert_eq!(errors.len(), 1);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_error_display() {
            let e = ParseError {
                input: "abc".to_string(),
                reason: "invalid".to_string(),
            };
            let msg = format!("{}", e);
            assert!(msg.contains("abc"));
        }
    
        #[test]
        fn test_parse_error_is_error_trait() {
            let e: Box<dyn Error> = Box::new(ParseError {
                input: "x".to_string(),
                reason: "bad".to_string(),
            });
            assert!(e.source().is_none());
        }
    
        #[test]
        fn test_validation_error_source() {
            let result = validate_user_age("abc");
            assert!(result.is_err());
            let e = result.unwrap_err();
            assert!(e.source().is_some());
        }
    
        #[test]
        fn test_valid_age() {
            assert_eq!(validate_user_age("25").unwrap(), 25);
        }
    
        #[test]
        fn test_error_chain_string() {
            let result = validate_user_age("bad");
            if let Err(e) = result {
                let chain = print_error_chain(&e);
                assert!(chain.contains("validation failed"));
                assert!(chain.contains("Caused by"));
            }
        }
    
        #[test]
        fn test_collect_heterogeneous() {
            let errors = collect_errors();
            assert_eq!(errors.len(), 1);
        }
    }

    Deep Comparison

    OCaml vs Rust: std::error::Error Trait

    Pattern 1: Basic Error Implementation

    Rust

    use std::error::Error;
    use std::fmt;
    
    #[derive(Debug)]
    struct ParseError { input: String, reason: String }
    
    impl fmt::Display for ParseError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "failed to parse '{}': {}", self.input, self.reason)
        }
    }
    
    impl Error for ParseError {}  // source() defaults to None
    

    Pattern 2: Error with Source Chain

    OCaml

    exception ChainedError of string * exn
    
    let with_context msg result =
      match result with
      | Ok _ as r -> r
      | Error e -> Error (ChainedError (msg, e))
    

    Rust

    #[derive(Debug)]
    struct ValidationError {
        field: String,
        source: Box<dyn Error + Send + Sync>,
    }
    
    impl Error for ValidationError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(self.source.as_ref())
        }
    }
    

    Pattern 3: Dynamic Error Collection

    Rust

    let errors: Vec<Box<dyn Error>> = vec![
        Box::new(ParseError { ... }),
        Box::new(IoError { ... }),
    ];
    

    Key Differences

    ConceptOCamlRust
    Error traitNo standardstd::error::Error
    RequirementsNoneDisplay + Debug
    Error chainingManual cause fieldsource() method
    Dynamic dispatchExceptions are polymorphicBox<dyn Error>
    Walk chainManual traversalLoop over .source()

    Exercises

  • Implement std::error::Error for a three-level error chain: IoError wrapping ParseError wrapping ValidationError, and traverse the chain using source().
  • Write a function print_error_chain(e: &dyn Error) that iterates through source() links and prints each cause on a new line.
  • Implement a custom error that wraps std::io::Error using Box<dyn Error + Send + Sync> and test that source() exposes the original IO error.
  • Open Source Repos