ExamplesBy LevelBy TopicLearning Paths
296 Intermediate

296: From Trait for Error Conversion

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "296: From Trait for Error Conversion" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Functions calling multiple libraries encounter multiple error types. Key difference from OCaml: 1. **Automatic conversion**: Rust's `?` calls `From::from()` at every error propagation point — zero boilerplate after the `impl From`; OCaml requires manual wrapping.

Tutorial

The Problem

Functions calling multiple libraries encounter multiple error types. Unifying them into a single application error type with match on every ? usage is boilerplate. The From<SourceError> trait enables automatic conversion: when impl From<ParseIntError> for AppError is defined, the ? operator automatically calls AppError::from(e) when propagating a ParseIntError. This is how Rust achieves zero-boilerplate error type unification.

🎯 Learning Outcomes

  • • Understand that impl From<E> for AppError enables automatic ? conversion from E
  • • Implement From for each error type a function might encounter
  • • Recognize the ? desugaring as Err(AppError::from(e))
  • • Use From to build layered error hierarchies without explicit mapping at each call site
  • Code Example

    impl From<ParseIntError> for AppError {
        fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
    }
    
    fn process(s: &str) -> Result<i32, AppError> {
        let n = s.parse::<i32>()?;  // auto-converts via From
        Ok(n)
    }

    Key Differences

  • Automatic conversion: Rust's ? calls From::from() at every error propagation point — zero boilerplate after the impl From; OCaml requires manual wrapping.
  • Type hierarchy: From implementations create a directed conversion graph; adding a new library error requires one new impl From.
  • Conflict prevention: Only one From<E> impl per target type is allowed — prevents ambiguous conversions.
  • **Into derived**: impl From<A> for B automatically provides impl Into<B> for A — both directions are covered.
  • OCaml Approach

    OCaml does not have automatic error conversion. Each call site must explicitly wrap errors:

    let process s =
      match int_of_string_opt s with
      | None -> Error (`Parse "not a number")
      | Some n ->
        if n < 0 then Error (`Logic "negative")
        else Ok (n * 2)
    

    Libraries like Error_monad (from Tezos) provide richer error composition, but there is no standard mechanism for automatic conversion.

    Full Source

    #![allow(clippy::all)]
    //! # From Trait for Error Conversion
    //!
    //! `impl From<E> for MyErr` enables automatic error conversion via `?`.
    
    use std::fmt;
    use std::num::ParseIntError;
    
    #[derive(Debug)]
    pub enum AppError {
        Parse(ParseIntError),
        Logic(String),
    }
    
    impl fmt::Display for AppError {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                AppError::Parse(e) => write!(f, "parse error: {}", e),
                AppError::Logic(s) => write!(f, "logic error: {}", s),
            }
        }
    }
    
    // These From impls make `?` work seamlessly
    impl From<ParseIntError> for AppError {
        fn from(e: ParseIntError) -> Self {
            AppError::Parse(e)
        }
    }
    
    /// Parse a number - returns ParseIntError
    pub fn parse_number(s: &str) -> Result<i32, ParseIntError> {
        s.trim().parse()
    }
    
    /// Validate that a number is positive
    pub fn validate_positive(n: i32) -> Result<i32, AppError> {
        if n > 0 {
            Ok(n)
        } else {
            Err(AppError::Logic(format!("{} is not positive", n)))
        }
    }
    
    /// Process input - ? converts ParseIntError -> AppError automatically via From
    pub fn process(input: &str) -> Result<i32, AppError> {
        let n = parse_number(input)?; // From<ParseIntError> called
        let validated = validate_positive(n)?;
        Ok(validated * 2)
    }
    
    /// Alternative: explicit conversion with .into()
    pub fn process_explicit(input: &str) -> Result<i32, AppError> {
        let n = parse_number(input).map_err(AppError::from)?;
        validate_positive(n).map(|v| v * 2)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_process_ok() {
            assert_eq!(process("21").unwrap(), 42);
        }
    
        #[test]
        fn test_process_parse_err() {
            assert!(matches!(process("abc"), Err(AppError::Parse(_))));
        }
    
        #[test]
        fn test_process_logic_err() {
            assert!(matches!(process("-1"), Err(AppError::Logic(_))));
        }
    
        #[test]
        fn test_from_conversion() {
            let e: ParseIntError = "x".parse::<i32>().unwrap_err();
            let app: AppError = e.into(); // via From
            assert!(matches!(app, AppError::Parse(_)));
        }
    
        #[test]
        fn test_process_whitespace() {
            assert_eq!(process("  42  ").unwrap(), 84);
        }
    
        #[test]
        fn test_process_explicit_same() {
            assert_eq!(process_explicit("21").unwrap(), 42);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_process_ok() {
            assert_eq!(process("21").unwrap(), 42);
        }
    
        #[test]
        fn test_process_parse_err() {
            assert!(matches!(process("abc"), Err(AppError::Parse(_))));
        }
    
        #[test]
        fn test_process_logic_err() {
            assert!(matches!(process("-1"), Err(AppError::Logic(_))));
        }
    
        #[test]
        fn test_from_conversion() {
            let e: ParseIntError = "x".parse::<i32>().unwrap_err();
            let app: AppError = e.into(); // via From
            assert!(matches!(app, AppError::Parse(_)));
        }
    
        #[test]
        fn test_process_whitespace() {
            assert_eq!(process("  42  ").unwrap(), 84);
        }
    
        #[test]
        fn test_process_explicit_same() {
            assert_eq!(process_explicit("21").unwrap(), 42);
        }
    }

    Deep Comparison

    OCaml vs Rust: From Trait for Errors

    Pattern 1: Automatic Conversion

    OCaml

    (* Must manually convert at every call site *)
    let process s =
      let* n = parse_number s |> Result.map_error (fun e -> ParseErr e) in
      let* v = validate n in
      Ok v
    

    Rust

    impl From<ParseIntError> for AppError {
        fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
    }
    
    fn process(s: &str) -> Result<i32, AppError> {
        let n = s.parse::<i32>()?;  // auto-converts via From
        Ok(n)
    }
    

    Pattern 2: Manual vs Implicit

    OCaml

    Result.map_error (fun e -> wrap e) result
    

    Rust

    result?  // Calls From::from() automatically
    

    Key Differences

    ConceptOCamlRust
    ConversionResult.map_error at each siteimpl From<E> once
    TriggeringExplicitImplicit via ?
    Type inferenceN/ACompiler selects From impl
    BoilerplateScatteredCentralized

    Exercises

  • Define an AppError with four variants and implement From for each of: std::io::Error, std::num::ParseIntError, serde_json::Error (simulated), and String.
  • Show that without a From impl, the ? operator fails to compile, then add the impl and verify it compiles.
  • Implement a function that calls three different libraries and propagates all their errors through a single AppError using ? without any explicit map_err.
  • Open Source Repos