307: Error Propagation in Closures
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
? inside a closure propagates from the closure, not the outer functionmap(|s| s.parse::<i32>()) to produce Iterator<Item = Result<i32, E>>collect::<Result<Vec<_>, _>>()filter_map(|s| s.parse().ok()) to silently drop errorsCode 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
? propagates to the enclosing function; inside a closure passed to map(), it propagates from the closure (making the closure return Result).? operator requires the closure to return Result<T, E> or Option<T> — it doesn't work in closures returning plain T.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());
}
}#[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
&[&str] into numbers, doubles them, and collects both parsed values and unparseable strings into separate Vecs in a single pass.try_map<T, U, E>(v: Vec<T>, f: impl Fn(T) -> Result<U, E>) -> Result<Vec<U>, E> using iterator combinators.? inside a closure does NOT propagate to the outer function by using it inside a map() closure.