ExamplesBy LevelBy TopicLearning Paths
307 Intermediate

307: Error Propagation in Closures

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "307: Error Propagation in Closures" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The `?` operator propagates errors from the enclosing function. Key difference from OCaml: 1. **Closure boundary**: `?` propagates to the enclosing function; inside a closure passed to `map()`, it propagates from the closure (making the closure return `Result`).

Tutorial

The Problem

The ? operator propagates errors from the enclosing function. Inside a closure passed to map() or and_then(), ? propagates from the closure, not the outer function. This distinction matters for Iterator::map(): the closure returns Result<T, E>, not T, and the results must be collected or handled. Understanding how errors flow through closures is essential for writing correct iterator pipelines over fallible operations.

🎯 Learning Outcomes

  • • Understand that ? inside a closure propagates from the closure, not the outer function
  • • Use map(|s| s.parse::<i32>()) to produce Iterator<Item = Result<i32, E>>
  • • Collect results with short-circuiting via collect::<Result<Vec<_>, _>>()
  • • Use filter_map(|s| s.parse().ok()) to silently drop errors
  • Code Example

    #![allow(clippy::all)]
    //! # Error Propagation in Closures
    //!
    //! `?` in closures requires the closure to return `Result`/`Option`.
    
    /// Parse number from string
    pub fn parse_number(s: &str) -> Result<i32, String> {
        s.trim()
            .parse::<i32>()
            .map_err(|_| format!("not a number: '{}'", s))
    }
    
    /// Collect results - short-circuits on first error
    pub fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, String> {
        inputs.iter().map(|s| parse_number(s)).collect()
    }
    
    /// Filter and keep only valid parses (drops errors)
    pub fn parse_valid(inputs: &[&str]) -> Vec<i32> {
        inputs
            .iter()
            .filter_map(|s| s.parse::<i32>().ok())
            .collect()
    }
    
    /// Try fold for short-circuit accumulation
    pub fn sum_all(inputs: &[&str]) -> Result<i32, String> {
        inputs
            .iter()
            .try_fold(0i32, |acc, s| Ok(acc + parse_number(s)?))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_all_ok() {
            let result = parse_all(&["1", "2", "3"]);
            assert_eq!(result.unwrap(), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_parse_all_err() {
            let result = parse_all(&["1", "bad", "3"]);
            assert!(result.is_err());
        }
    
        #[test]
        fn test_parse_valid() {
            let result = parse_valid(&["1", "bad", "3"]);
            assert_eq!(result, vec![1, 3]);
        }
    
        #[test]
        fn test_sum_all_ok() {
            let result = sum_all(&["1", "2", "3"]);
            assert_eq!(result.unwrap(), 6);
        }
    
        #[test]
        fn test_sum_all_err() {
            let result = sum_all(&["1", "x", "3"]);
            assert!(result.is_err());
        }
    }

    Key Differences

  • Closure boundary: ? propagates to the enclosing function; inside a closure passed to map(), it propagates from the closure (making the closure return Result).
  • Two strategies: Collect-with-short-circuit vs filter-and-continue are the two fundamental choices for handling errors in iterator pipelines.
  • Closure return type: The ? operator requires the closure to return Result<T, E> or Option<T> — it doesn't work in closures returning plain T.
  • Accumulating errors: To collect all errors (not just the first), use fold() building up a Vec<E> instead of collect::<Result<_, _>>.
  • OCaml Approach

    OCaml's let* binding with Seq or List functions provides similar behavior — errors propagate within the let* chain, not from closures:

    let parse_all inputs =
      List.fold_right (fun s acc ->
        let* lst = acc in
        let* n = parse_number s in
        Ok (n :: lst)
      ) inputs (Ok [])
    

    Full Source

    #![allow(clippy::all)]
    //! # Error Propagation in Closures
    //!
    //! `?` in closures requires the closure to return `Result`/`Option`.
    
    /// Parse number from string
    pub fn parse_number(s: &str) -> Result<i32, String> {
        s.trim()
            .parse::<i32>()
            .map_err(|_| format!("not a number: '{}'", s))
    }
    
    /// Collect results - short-circuits on first error
    pub fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, String> {
        inputs.iter().map(|s| parse_number(s)).collect()
    }
    
    /// Filter and keep only valid parses (drops errors)
    pub fn parse_valid(inputs: &[&str]) -> Vec<i32> {
        inputs
            .iter()
            .filter_map(|s| s.parse::<i32>().ok())
            .collect()
    }
    
    /// Try fold for short-circuit accumulation
    pub fn sum_all(inputs: &[&str]) -> Result<i32, String> {
        inputs
            .iter()
            .try_fold(0i32, |acc, s| Ok(acc + parse_number(s)?))
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_all_ok() {
            let result = parse_all(&["1", "2", "3"]);
            assert_eq!(result.unwrap(), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_parse_all_err() {
            let result = parse_all(&["1", "bad", "3"]);
            assert!(result.is_err());
        }
    
        #[test]
        fn test_parse_valid() {
            let result = parse_valid(&["1", "bad", "3"]);
            assert_eq!(result, vec![1, 3]);
        }
    
        #[test]
        fn test_sum_all_ok() {
            let result = sum_all(&["1", "2", "3"]);
            assert_eq!(result.unwrap(), 6);
        }
    
        #[test]
        fn test_sum_all_err() {
            let result = sum_all(&["1", "x", "3"]);
            assert!(result.is_err());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_all_ok() {
            let result = parse_all(&["1", "2", "3"]);
            assert_eq!(result.unwrap(), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_parse_all_err() {
            let result = parse_all(&["1", "bad", "3"]);
            assert!(result.is_err());
        }
    
        #[test]
        fn test_parse_valid() {
            let result = parse_valid(&["1", "bad", "3"]);
            assert_eq!(result, vec![1, 3]);
        }
    
        #[test]
        fn test_sum_all_ok() {
            let result = sum_all(&["1", "2", "3"]);
            assert_eq!(result.unwrap(), 6);
        }
    
        #[test]
        fn test_sum_all_err() {
            let result = sum_all(&["1", "x", "3"]);
            assert!(result.is_err());
        }
    }

    Deep Comparison

    error-propagation-closures

    See README.md for details.

    Exercises

  • Write a pipeline that parses &[&str] into numbers, doubles them, and collects both parsed values and unparseable strings into separate Vecs in a single pass.
  • Implement try_map<T, U, E>(v: Vec<T>, f: impl Fn(T) -> Result<U, E>) -> Result<Vec<U>, E> using iterator combinators.
  • Demonstrate the closure boundary rule: show that ? inside a closure does NOT propagate to the outer function by using it inside a map() closure.
  • Open Source Repos