ExamplesBy LevelBy TopicLearning Paths
1004 Intermediate

1004 — Error Conversion

Functional Programming

Tutorial

The Problem

Implement From<SubError> for AppError to enable automatic error conversion with the ? operator. When a function returns Result<T, AppError> and calls a sub-function returning Result<T, ParseIntError>, the ? operator automatically wraps the inner error via From::from. Compare with OCaml's explicit manual wrapping.

🎯 Learning Outcomes

  • • Implement impl From<IoError> for AppError and impl From<ParseIntError> for AppError
  • • Understand that ? desugars to map_err(From::from) — calling the From impl
  • • Implement Error::source to expose the wrapped error for error chain inspection
  • • Chain multiple ? calls in a single function without explicit map_err
  • • Map Rust's automatic From-based conversion to OCaml's manual IoError(e) wrapping
  • • Recognise the AppError unified error enum pattern as the idiomatic Rust design
  • Code Example

    #![allow(clippy::all)]
    // 1004: Error Conversion
    // From trait for automatic error conversion with ? operator
    
    use std::fmt;
    use std::num::ParseIntError;
    
    // Sub-error types
    #[derive(Debug)]
    enum IoError {
        FileNotFound(String),
        PermissionDenied(String),
    }
    
    impl fmt::Display for IoError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                IoError::FileNotFound(p) => write!(f, "file not found: {}", p),
                IoError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
            }
        }
    }
    
    impl std::error::Error for IoError {}
    
    // Unified app error with From impls
    #[derive(Debug)]
    enum AppError {
        Io(IoError),
        Parse(ParseIntError),
    }
    
    impl fmt::Display for AppError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                AppError::Io(e) => write!(f, "IO: {}", e),
                AppError::Parse(e) => write!(f, "Parse: {}", e),
            }
        }
    }
    
    impl std::error::Error for AppError {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            match self {
                AppError::Io(e) => Some(e),
                AppError::Parse(e) => Some(e),
            }
        }
    }
    
    // From impls enable automatic conversion with ?
    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)
        }
    }
    
    // Functions that return sub-errors
    fn read_config(path: &str) -> Result<String, IoError> {
        if path == "/missing" {
            Err(IoError::FileNotFound(path.to_string()))
        } else {
            Ok("42".to_string())
        }
    }
    
    // The ? operator automatically calls From to convert errors
    fn load_config(path: &str) -> Result<i64, AppError> {
        let content = read_config(path)?; // IoError -> AppError via From
        let value: i64 = content.parse()?; // ParseIntError -> AppError via From
        Ok(value)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_successful_load() {
            assert_eq!(load_config("/ok").unwrap(), 42);
        }
    
        #[test]
        fn test_io_error_conversion() {
            let result = load_config("/missing");
            assert!(result.is_err());
            let err = result.unwrap_err();
            assert!(matches!(err, AppError::Io(IoError::FileNotFound(_))));
            assert!(err.to_string().contains("file not found"));
        }
    
        #[test]
        fn test_from_io_error() {
            let io_err = IoError::FileNotFound("test".into());
            let app_err: AppError = io_err.into();
            assert!(matches!(app_err, AppError::Io(_)));
        }
    
        #[test]
        fn test_from_parse_error() {
            let parse_err: ParseIntError = "abc".parse::<i64>().unwrap_err();
            let app_err: AppError = parse_err.into();
            assert!(matches!(app_err, AppError::Parse(_)));
        }
    
        #[test]
        fn test_error_source_chain() {
            use std::error::Error;
            let result = load_config("/missing");
            let err = result.unwrap_err();
            // source() returns the inner error
            assert!(err.source().is_some());
        }
    
        #[test]
        fn test_question_mark_converts() {
            fn inner() -> Result<i64, AppError> {
                let _s = read_config("/ok")?; // auto-converts IoError
                Ok(42)
            }
            assert_eq!(inner().unwrap(), 42);
        }
    }

    Key Differences

    AspectRustOCaml
    Auto-conversionFrom impl + ?Manual wrapping IoError(e)
    ? operatormap_err(From::from) + early returnlet* bind + manual error lifting
    Error chainError::source()No standard protocol
    Wrapper enumAppError::Io(e), AppError::Parse(e)Same variant wrapping
    From boilerplate5-line impl per error typeManual fun e -> IoError e at each call
    thiserror#[from] attribute eliminates FromNo equivalent

    The From + ? pattern is one of Rust's most important ergonomic features. Writing some_fallible_call()? in a function returning Result<T, AppError> automatically converts any matching sub-error type. This enables clean, readable error propagation without noise.

    OCaml Approach

    OCaml wraps errors manually: Error (IoError (FileNotFound path)). There is no ?-equivalent or automatic conversion. Functions returning app_error Result must explicitly tag sub-errors: Result.map_error (fun e -> IoError e) (read_file path). OCaml 4.08+ provides let* (monadic bind) for result chaining, but conversion is still explicit.

    Full Source

    #![allow(clippy::all)]
    // 1004: Error Conversion
    // From trait for automatic error conversion with ? operator
    
    use std::fmt;
    use std::num::ParseIntError;
    
    // Sub-error types
    #[derive(Debug)]
    enum IoError {
        FileNotFound(String),
        PermissionDenied(String),
    }
    
    impl fmt::Display for IoError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                IoError::FileNotFound(p) => write!(f, "file not found: {}", p),
                IoError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
            }
        }
    }
    
    impl std::error::Error for IoError {}
    
    // Unified app error with From impls
    #[derive(Debug)]
    enum AppError {
        Io(IoError),
        Parse(ParseIntError),
    }
    
    impl fmt::Display for AppError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                AppError::Io(e) => write!(f, "IO: {}", e),
                AppError::Parse(e) => write!(f, "Parse: {}", e),
            }
        }
    }
    
    impl std::error::Error for AppError {
        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
            match self {
                AppError::Io(e) => Some(e),
                AppError::Parse(e) => Some(e),
            }
        }
    }
    
    // From impls enable automatic conversion with ?
    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)
        }
    }
    
    // Functions that return sub-errors
    fn read_config(path: &str) -> Result<String, IoError> {
        if path == "/missing" {
            Err(IoError::FileNotFound(path.to_string()))
        } else {
            Ok("42".to_string())
        }
    }
    
    // The ? operator automatically calls From to convert errors
    fn load_config(path: &str) -> Result<i64, AppError> {
        let content = read_config(path)?; // IoError -> AppError via From
        let value: i64 = content.parse()?; // ParseIntError -> AppError via From
        Ok(value)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_successful_load() {
            assert_eq!(load_config("/ok").unwrap(), 42);
        }
    
        #[test]
        fn test_io_error_conversion() {
            let result = load_config("/missing");
            assert!(result.is_err());
            let err = result.unwrap_err();
            assert!(matches!(err, AppError::Io(IoError::FileNotFound(_))));
            assert!(err.to_string().contains("file not found"));
        }
    
        #[test]
        fn test_from_io_error() {
            let io_err = IoError::FileNotFound("test".into());
            let app_err: AppError = io_err.into();
            assert!(matches!(app_err, AppError::Io(_)));
        }
    
        #[test]
        fn test_from_parse_error() {
            let parse_err: ParseIntError = "abc".parse::<i64>().unwrap_err();
            let app_err: AppError = parse_err.into();
            assert!(matches!(app_err, AppError::Parse(_)));
        }
    
        #[test]
        fn test_error_source_chain() {
            use std::error::Error;
            let result = load_config("/missing");
            let err = result.unwrap_err();
            // source() returns the inner error
            assert!(err.source().is_some());
        }
    
        #[test]
        fn test_question_mark_converts() {
            fn inner() -> Result<i64, AppError> {
                let _s = read_config("/ok")?; // auto-converts IoError
                Ok(42)
            }
            assert_eq!(inner().unwrap(), 42);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_successful_load() {
            assert_eq!(load_config("/ok").unwrap(), 42);
        }
    
        #[test]
        fn test_io_error_conversion() {
            let result = load_config("/missing");
            assert!(result.is_err());
            let err = result.unwrap_err();
            assert!(matches!(err, AppError::Io(IoError::FileNotFound(_))));
            assert!(err.to_string().contains("file not found"));
        }
    
        #[test]
        fn test_from_io_error() {
            let io_err = IoError::FileNotFound("test".into());
            let app_err: AppError = io_err.into();
            assert!(matches!(app_err, AppError::Io(_)));
        }
    
        #[test]
        fn test_from_parse_error() {
            let parse_err: ParseIntError = "abc".parse::<i64>().unwrap_err();
            let app_err: AppError = parse_err.into();
            assert!(matches!(app_err, AppError::Parse(_)));
        }
    
        #[test]
        fn test_error_source_chain() {
            use std::error::Error;
            let result = load_config("/missing");
            let err = result.unwrap_err();
            // source() returns the inner error
            assert!(err.source().is_some());
        }
    
        #[test]
        fn test_question_mark_converts() {
            fn inner() -> Result<i64, AppError> {
                let _s = read_config("/ok")?; // auto-converts IoError
                Ok(42)
            }
            assert_eq!(inner().unwrap(), 42);
        }
    }

    Deep Comparison

    Error Conversion — Comparison

    Core Insight

    Rust's From trait + ? operator automates what OCaml forces you to do manually: wrapping sub-errors into a unified error type.

    OCaml Approach

  • • Must manually wrap each sub-error: Error (IoError e) at every call site
  • • Can write lift_* helper functions but they're boilerplate
  • • No language-level support for automatic error conversion
  • • Each new error source means another wrapper call
  • Rust Approach

  • • Implement From<SubError> for UnifiedError once per sub-error type
  • • The ? operator automatically calls .into() which uses From
  • • Adding a new error source = one new From impl, zero call-site changes
  • • The source() method preserves the error chain
  • Comparison Table

    AspectOCamlRust
    Conversion mechanismManual wrappingFrom trait + ?
    Boilerplate per call siteOne wrapper per callZero (automatic)
    Adding new error sourceTouch every call siteOne From impl
    Error chainManual nestingsource() method
    Type safetyVariant pattern matchSame + compiler enforced

    Exercises

  • Add a third sub-error DbError(String) to AppError with a From<DbError> for AppError impl.
  • Implement fn process_all(items: Vec<&str>) -> Result<Vec<i32>, AppError> that parses all items, collecting the first error.
  • Use Result::map_err manually to convert an IoError to AppError without the From impl, and compare verbosity.
  • Add impl std::error::Error::source chaining for three levels: AppErrorIoErrorstd::io::Error.
  • In OCaml, implement map_error : ('a -> 'b) -> ('c, 'a) result -> ('c, 'b) result and use it to build a clean error-lifting pipeline.
  • Open Source Repos