ExamplesBy LevelBy TopicLearning Paths
300 Intermediate

300: Chaining Errors with source()

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "300: Chaining Errors with source()" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Errors in real systems are causal chains: a configuration loading failure is caused by a file read failure, which is caused by a permissions denial. Key difference from OCaml: 1. **Standard protocol**: `source()` is a standard trait method — any library that implements it participates in the chain automatically.

Tutorial

The Problem

Errors in real systems are causal chains: a configuration loading failure is caused by a file read failure, which is caused by a permissions denial. Displaying only the top-level error loses the root cause. The Error::source() method creates a linked list of errors from high-level to low-level, enabling tools and users to see the complete causal chain. This is the Rust equivalent of Java's exception chaining (getCause()) and Python's raise X from Y.

🎯 Learning Outcomes

  • • Implement Error::source() to expose a wrapped inner error as the cause
  • • Traverse an error chain using the source() method iteratively
  • • Build a print_error_chain function that displays the full causal hierarchy
  • • Understand the ownership model: source() returns &(dyn Error + 'static)
  • Code Example

    impl Error for ConfigError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(&self.source)
        }
    }

    Key Differences

  • Standard protocol: source() is a standard trait method — any library that implements it participates in the chain automatically.
  • Traversal: Rust provides no built-in chain display; users write while let Some(s) = e.source() loops.
  • Ownership: source() returns a borrowed reference &(dyn Error + 'static) — the chain is borrowed, not owned, preventing double-free issues.
  • Future: The Backtrace type (stabilized in Rust 1.73) captures stack traces at error creation, complementing the causal chain.
  • OCaml Approach

    OCaml has no standard error chaining. Exceptions have a Printexc.raise_with_backtrace for preserving stack traces, but error values in Result require explicit nesting:

    type 'a with_cause = { error: 'a; cause: exn option }
    (* Custom traversal required; no standard chain protocol *)
    

    Full Source

    #![allow(clippy::all)]
    //! # Chaining Errors with source()
    //!
    //! `Error::source()` creates a linked list of causes — traverse to print full chain.
    
    use std::error::Error;
    use std::fmt;
    
    /// File error - root cause
    #[derive(Debug)]
    pub struct FileError {
        pub path: String,
    }
    
    impl fmt::Display for FileError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "file '{}' not found", self.path)
        }
    }
    
    impl Error for FileError {}
    
    /// Config error - wraps FileError
    #[derive(Debug)]
    pub struct ConfigError {
        pub source: FileError,
    }
    
    impl fmt::Display for ConfigError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "failed to read configuration")
        }
    }
    
    impl Error for ConfigError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(&self.source)
        }
    }
    
    /// Startup error - wraps ConfigError
    #[derive(Debug)]
    pub struct StartupError {
        pub source: ConfigError,
    }
    
    impl fmt::Display for StartupError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "application startup failed")
        }
    }
    
    impl Error for StartupError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(&self.source)
        }
    }
    
    /// Walk the source() chain and format each error
    pub fn format_error_chain(e: &dyn Error) -> String {
        let mut result = format!("Error: {}", e);
        let mut cause = e.source();
        let mut depth = 1;
        while let Some(c) = cause {
            result.push_str(&format!("\n{}Caused by: {}", "  ".repeat(depth), c));
            cause = c.source();
            depth += 1;
        }
        result
    }
    
    /// Collect the full error chain into a Vec
    pub fn error_chain(e: &dyn Error) -> Vec<String> {
        let mut chain = vec![e.to_string()];
        let mut cause = e.source();
        while let Some(c) = cause {
            chain.push(c.to_string());
            cause = c.source();
        }
        chain
    }
    
    /// Get the root cause
    pub fn root_cause(e: &dyn Error) -> &dyn Error {
        let mut current = e;
        while let Some(source) = current.source() {
            current = source;
        }
        current
    }
    
    /// Create a test error chain
    pub fn make_test_chain(path: &str) -> StartupError {
        StartupError {
            source: ConfigError {
                source: FileError {
                    path: path.to_string(),
                },
            },
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_chain_length() {
            let err = make_test_chain("config.toml");
            let chain = error_chain(&err);
            assert_eq!(chain.len(), 3);
        }
    
        #[test]
        fn test_root_cause_message() {
            let err = make_test_chain("missing.toml");
            let chain = error_chain(&err);
            assert!(chain.last().unwrap().contains("missing.toml"));
        }
    
        #[test]
        fn test_source_none_for_root() {
            let e = FileError {
                path: "x".to_string(),
            };
            assert!(e.source().is_none());
        }
    
        #[test]
        fn test_format_chain() {
            let err = make_test_chain("app.conf");
            let formatted = format_error_chain(&err);
            assert!(formatted.contains("startup failed"));
            assert!(formatted.contains("configuration"));
            assert!(formatted.contains("app.conf"));
        }
    
        #[test]
        fn test_root_cause() {
            let err = make_test_chain("test.cfg");
            let root = root_cause(&err);
            assert!(root.to_string().contains("test.cfg"));
        }
    
        #[test]
        fn test_display_messages() {
            let err = make_test_chain("data.json");
            assert_eq!(format!("{}", err), "application startup failed");
            assert_eq!(format!("{}", err.source), "failed to read configuration");
            assert_eq!(
                format!("{}", err.source.source),
                "file 'data.json' not found"
            );
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_chain_length() {
            let err = make_test_chain("config.toml");
            let chain = error_chain(&err);
            assert_eq!(chain.len(), 3);
        }
    
        #[test]
        fn test_root_cause_message() {
            let err = make_test_chain("missing.toml");
            let chain = error_chain(&err);
            assert!(chain.last().unwrap().contains("missing.toml"));
        }
    
        #[test]
        fn test_source_none_for_root() {
            let e = FileError {
                path: "x".to_string(),
            };
            assert!(e.source().is_none());
        }
    
        #[test]
        fn test_format_chain() {
            let err = make_test_chain("app.conf");
            let formatted = format_error_chain(&err);
            assert!(formatted.contains("startup failed"));
            assert!(formatted.contains("configuration"));
            assert!(formatted.contains("app.conf"));
        }
    
        #[test]
        fn test_root_cause() {
            let err = make_test_chain("test.cfg");
            let root = root_cause(&err);
            assert!(root.to_string().contains("test.cfg"));
        }
    
        #[test]
        fn test_display_messages() {
            let err = make_test_chain("data.json");
            assert_eq!(format!("{}", err), "application startup failed");
            assert_eq!(format!("{}", err.source), "failed to read configuration");
            assert_eq!(
                format!("{}", err.source.source),
                "file 'data.json' not found"
            );
        }
    }

    Deep Comparison

    OCaml vs Rust: Error Chaining

    Pattern: Source Chain

    Rust

    impl Error for ConfigError {
        fn source(&self) -> Option<&(dyn Error + 'static)> {
            Some(&self.source)
        }
    }
    

    OCaml

    type config_error = { cause: file_error option }
    (* No standard trait - manual chaining *)
    

    Pattern: Walking the Chain

    Rust

    let mut cause = e.source();
    while let Some(c) = cause {
        println!("  Caused by: {}", c);
        cause = c.source();
    }
    

    OCaml

    let rec walk_chain = function
      | None -> ()
      | Some e ->
        Printf.printf "  Caused by: %s\n" (string_of_error e);
        walk_chain e.cause
    

    Pattern: Finding Root Cause

    Rust

    fn root_cause(e: &dyn Error) -> &dyn Error {
        let mut current = e;
        while let Some(source) = current.source() {
            current = source;
        }
        current
    }
    

    Key Differences

    ConceptOCamlRust
    Chain mechanismManual cause fieldError::source()
    Standard interfaceNonestd::error::Error trait
    Root causeManual traversalSame pattern works
    Pretty printingCustom functionLoop over source()
    PolymorphismException hierarchydyn Error trait object

    Exercises

  • Build a three-level error chain (AppError -> ConfigError -> IoError) and implement source() at each level to expose the next.
  • Write a collect_error_chain(e: &dyn Error) -> Vec<String> function that collects all error messages in the chain as a vector.
  • Implement an error_root_cause(e: &dyn Error) -> &dyn Error function that traverses source() links until it reaches the last error with no source.
  • Open Source Repos