๐Ÿฆ€ Functional Rust
๐ŸŽฌ Error Handling in Rust Option, Result, the ? operator, and combinators.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Option represents a value that may or may not exist โ€” Some(value) or None

โ€ข Result represents success (Ok) or failure (Err) โ€” no exceptions needed

โ€ข The ? operator propagates errors up the call stack concisely

โ€ข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations

โ€ข The compiler forces you to handle every error case โ€” no silent failures

048: Error Propagation

Difficulty: 1 Level: Foundations The `?` operator โ€” the most important operator in Rust โ€” propagates errors up the call stack with a single character.

The Problem This Solves

You're writing a function that calls three other functions, each of which can fail. Without any special syntax, you'd write:
let a = match parse_int(a_str) {
 Ok(v) => v,
 Err(e) => return Err(e),
};
let b = match parse_int(b_str) {
 Ok(v) => v,
 Err(e) => return Err(e),
};
That's 10 lines of boilerplate for two fallible calls. With five steps, you've written 25 lines of match arms that all do the same thing: "if error, return it." The actual logic โ€” what you're computing โ€” is drowned out. Java's checked exceptions were an attempt to solve this, but callers still have to write `throws` declarations everywhere or wrap everything in `RuntimeException`. Python's exceptions propagate automatically, but that means they're also invisible โ€” you can't tell from reading code which calls might throw. Rust finds the middle ground: errors propagate explicitly with `?`, but the propagation is opt-in, visible, and checked by the compiler.

The Intuition

The `?` operator does exactly one thing: "if this is `Err`, return it from the current function; if it's `Ok`, give me the value." It's not magic โ€” it desugars to a match + early return. But it collapses 5-line match blocks into one character. Think of it like Python's exception propagation, but you choose where it propagates. The `?` is visible in the source code. Every `?` is a place where a function can exit early. You can read a function and know exactly how many ways it can fail. The function must return `Result` (or `Option`) for `?` to work. That's the contract: if you want to propagate errors, your caller also needs to handle them. The type system enforces the chain.

How It Works in Rust

// Implementing From lets ? auto-convert error types
impl From<ParseIntError> for AppError {
 fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

fn parse_int(s: &str) -> Result<i64, AppError> {
 // The inner parse returns ParseIntError; ? converts it to AppError via From
 Ok(s.trim().parse::<i64>()?)
}

// Multi-step computation using ? at each step
fn compute(a_str: &str, b_str: &str) -> Result<i64, AppError> {
 let a = parse_int(a_str)?;        // if Err(AppError::Parse), return immediately
 let b = parse_int(b_str)?;        // same
 let quotient = safe_div(a, b)?;   // if Err(AppError::DivByZero), return immediately
 let validated = validate_range(quotient)?;  // same
 Ok(validated * 2)                 // only reached if all steps succeeded
}

// Compare: WITHOUT ? operator (5 steps, 25 lines)
fn compute_verbose(a_str: &str, b_str: &str) -> Result<i64, AppError> {
 let a = match parse_int(a_str) { Ok(v) => v, Err(e) => return Err(e) };
 let b = match parse_int(b_str) { Ok(v) => v, Err(e) => return Err(e) };
 // ... three more of these
}
// With ?: 5 lines. Without?: 15+ lines. Same behavior.

What This Unlocks

Key Differences

ConceptOCamlRust
Propagate error`let* x = r in ...` (binding operator)`let x = r?;`
Manual early return`match r with Error e -> Error e \Ok v -> ...``match r { Err(e) => return Err(e), Ok(v) => ... }`
Error type conversionManual wrapping or `Result.map_error`Automatic via `From<E>` trait + `?`
Where it worksAny expressionOnly in functions returning `Result` or `Option`
`main` functionReturns `unit`, handle errors manually`fn main() -> Result<(), E>` โ€” `?` works in main too
Chained context`Result.map_error (fun e -> ...)``.map_err(\e\...)` before `?`
// Error Propagation โ€” 99 Problems #48
// Use the ? operator to propagate errors through call stacks.

use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    Parse(ParseIntError),
    DivByZero,
    ValidationFailed(String),
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::Parse(e) => write!(f, "parse error: {}", e),
            AppError::DivByZero => write!(f, "division by zero"),
            AppError::ValidationFailed(msg) => write!(f, "validation failed: {}", msg),
        }
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

fn parse_int(s: &str) -> Result<i64, AppError> {
    // Using From<ParseIntError> via ? conversion
    Ok(s.trim().parse::<i64>()?)
}

fn safe_div(a: i64, b: i64) -> Result<i64, AppError> {
    if b == 0 { Err(AppError::DivByZero) } else { Ok(a / b) }
}

fn validate_range(x: i64) -> Result<i64, AppError> {
    if x > 0 && x < 10000 {
        Ok(x)
    } else {
        Err(AppError::ValidationFailed(format!("{} not in (0, 10000)", x)))
    }
}

/// Multi-step computation using ? at each step.
fn compute(a_str: &str, b_str: &str) -> Result<i64, AppError> {
    let a = parse_int(a_str)?;
    let b = parse_int(b_str)?;
    let quotient = safe_div(a, b)?;
    let validated = validate_range(quotient)?;
    Ok(validated * 2)
}

/// Process a list, collecting errors with context.
fn process_list(inputs: &[(&str, &str)]) -> Vec<Result<i64, String>> {
    inputs
        .iter()
        .map(|(a, b)| compute(a, b).map_err(|e| format!("({},{}) -> {}", a, b, e)))
        .collect()
}

fn main() {
    let cases = [
        ("100", "5"),
        ("100", "0"),
        ("abc", "5"),
        ("50000", "1"),
        ("50", "2"),
    ];

    println!("Computing results:");
    for (a, b) in &cases {
        match compute(a, b) {
            Ok(v) => println!("  compute({:?}, {:?}) = {}", a, b, v),
            Err(e) => println!("  compute({:?}, {:?}) = Error: {}", a, b, e),
        }
    }

    println!("\nProcess list:");
    for result in process_list(&cases) {
        println!("  {:?}", result);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_success() {
        assert_eq!(compute("100", "5"), Ok(40)); // 100/5=20, *2=40
    }

    #[test]
    fn test_div_zero() {
        assert!(matches!(compute("100", "0"), Err(AppError::DivByZero)));
    }

    #[test]
    fn test_parse_error() {
        assert!(matches!(compute("abc", "5"), Err(AppError::Parse(_))));
    }

    #[test]
    fn test_validation_error() {
        assert!(matches!(
            compute("50000", "1"),
            Err(AppError::ValidationFailed(_))
        ));
    }

    #[test]
    fn test_from_conversion() {
        let result = parse_int("not_a_number");
        assert!(matches!(result, Err(AppError::Parse(_))));
    }
}
(* Error Propagation *)
(* OCaml 99 Problems #48 *)

(* Implementation for example 48 *)

(* Tests *)
let () =
  (* Add tests *)
  print_endline "โœ“ OCaml tests passed"