ExamplesBy LevelBy TopicLearning Paths
259 Intermediate

Result Monad — Error Chaining

Monadic patterns

Tutorial Video

Text description (accessibility)

This video demonstrates the "Result Monad — Error Chaining" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Monadic patterns. Chain multiple validation steps on a string input — parsing, positivity, and parity — so that the first failure short-circuits the remaining checks and returns a descriptive error. Key difference from OCaml: 1. **Operator vs method:** OCaml uses a custom infix `>>=`; Rust uses the `.and_then()` method or the `?` operator — no operator overloading needed.

Tutorial

The Problem

Chain multiple validation steps on a string input — parsing, positivity, and parity — so that the first failure short-circuits the remaining checks and returns a descriptive error.

🎯 Learning Outcomes

  • Result::and_then is Rust's direct equivalent of OCaml's monadic bind (>>=)
  • • The ? operator desugars to early-return on Err, giving monadic sequencing with imperative syntax
  • • Validation pipelines map naturally to the railway-oriented programming metaphor
  • map_err converts foreign error types into owned String errors without allocating on the success path
  • 🦀 The Rust Way

    Rust's Result provides and_then as the standard bind combinator, making .and_then(check_positive).and_then(check_even) idiomatic. Alternatively, the ? operator gives the same short-circuit semantics with sequential imperative style. Both forms compile to equivalent machine code; ? is preferred in practice for readability.

    Code Example

    pub fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("Not an integer: {s}"))
    }
    
    pub fn check_positive(n: i64) -> Result<i64, String> {
        if n > 0 { Ok(n) } else { Err("Must be positive".to_string()) }
    }
    
    pub fn check_even(n: i64) -> Result<i64, String> {
        if n % 2 == 0 { Ok(n) } else { Err("Must be even".to_string()) }
    }
    
    pub fn validate_idiomatic(s: &str) -> Result<i64, String> {
        parse_int(s).and_then(check_positive).and_then(check_even)
    }

    Key Differences

  • Operator vs method: OCaml uses a custom infix >>=; Rust uses the .and_then() method or the ? operator — no operator overloading needed.
  • Error conversion: OCaml concatenates strings freely; Rust requires map_err to convert parse errors into the uniform String error type.
  • Syntax sugar: Rust's ? operator provides do-notation-style sequencing without a monad typeclass — each ? is an explicit bind step.
  • Ownership: Rust validation functions take i64 by value (Copy type), avoiding any borrow complications in the chain.
  • OCaml Approach

    OCaml defines a custom >>= infix operator on result that pattern-matches: Error values pass through untouched while Ok values are unwrapped and fed to the next function. The chain parse_int s >>= check_positive >>= check_even reads left-to-right and terminates at the first Error.

    Full Source

    #![allow(clippy::all)]
    // Solution 1: Idiomatic Rust — Result's built-in monadic combinator
    // `and_then` is Rust's bind (>>=) for Result: propagates Err, applies f to Ok
    pub fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("Not an integer: {s}"))
    }
    
    pub fn check_positive(n: i64) -> Result<i64, String> {
        if n > 0 {
            Ok(n)
        } else {
            Err("Must be positive".to_string())
        }
    }
    
    pub fn check_even(n: i64) -> Result<i64, String> {
        if n % 2 == 0 {
            Ok(n)
        } else {
            Err("Must be even".to_string())
        }
    }
    
    // Railway-oriented: each step either advances the train or diverts to the error track
    pub fn validate_idiomatic(s: &str) -> Result<i64, String> {
        parse_int(s).and_then(check_positive).and_then(check_even)
    }
    
    // Solution 2: Explicit bind — mirrors OCaml's >>= operator exactly
    // `bind` unpacks Ok and applies f, short-circuits on Err
    fn bind<T, U, E>(r: Result<T, E>, f: impl FnOnce(T) -> Result<U, E>) -> Result<U, E> {
        match r {
            Err(e) => Err(e),
            Ok(x) => f(x),
        }
    }
    
    pub fn validate_explicit(s: &str) -> Result<i64, String> {
        bind(bind(parse_int(s), check_positive), check_even)
    }
    
    // Solution 3: Using the `?` operator — Rust's ergonomic monadic shorthand
    // `?` early-returns Err if the value is Err, like >>= but with explicit control flow
    pub fn validate_question_mark(s: &str) -> Result<i64, String> {
        let n = parse_int(s)?;
        let n = check_positive(n)?;
        check_even(n)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid_positive_even_succeeds() {
            assert_eq!(validate_idiomatic("42"), Ok(42));
            assert_eq!(validate_explicit("42"), Ok(42));
            assert_eq!(validate_question_mark("42"), Ok(42));
        }
    
        #[test]
        fn test_negative_number_fails_check_positive() {
            let expected = Err("Must be positive".to_string());
            assert_eq!(validate_idiomatic("-3"), expected);
            assert_eq!(validate_explicit("-3"), expected);
            assert_eq!(validate_question_mark("-3"), expected);
        }
    
        #[test]
        fn test_non_integer_string_fails_parse() {
            let expected = Err("Not an integer: abc".to_string());
            assert_eq!(validate_idiomatic("abc"), expected);
            assert_eq!(validate_explicit("abc"), expected);
            assert_eq!(validate_question_mark("abc"), expected);
        }
    
        #[test]
        fn test_positive_odd_fails_check_even() {
            let expected = Err("Must be even".to_string());
            assert_eq!(validate_idiomatic("7"), expected);
            assert_eq!(validate_explicit("7"), expected);
            assert_eq!(validate_question_mark("7"), expected);
        }
    
        #[test]
        fn test_zero_fails_check_positive() {
            // 0 is not > 0
            let expected = Err("Must be positive".to_string());
            assert_eq!(validate_idiomatic("0"), expected);
            assert_eq!(validate_explicit("0"), expected);
            assert_eq!(validate_question_mark("0"), expected);
        }
    
        #[test]
        fn test_parse_int_valid() {
            assert_eq!(parse_int("100"), Ok(100));
            assert_eq!(parse_int("-5"), Ok(-5));
            assert_eq!(parse_int("0"), Ok(0));
        }
    
        #[test]
        fn test_parse_int_invalid() {
            assert!(parse_int("abc").is_err());
            assert!(parse_int("1.5").is_err());
            assert!(parse_int("").is_err());
        }
    
        #[test]
        fn test_error_stops_at_first_failure() {
            // "abc" fails parse_int — check_positive and check_even never run
            // confirmed by the error message being about parsing, not positivity/parity
            let result = validate_idiomatic("abc");
            assert!(result.unwrap_err().contains("Not an integer"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_valid_positive_even_succeeds() {
            assert_eq!(validate_idiomatic("42"), Ok(42));
            assert_eq!(validate_explicit("42"), Ok(42));
            assert_eq!(validate_question_mark("42"), Ok(42));
        }
    
        #[test]
        fn test_negative_number_fails_check_positive() {
            let expected = Err("Must be positive".to_string());
            assert_eq!(validate_idiomatic("-3"), expected);
            assert_eq!(validate_explicit("-3"), expected);
            assert_eq!(validate_question_mark("-3"), expected);
        }
    
        #[test]
        fn test_non_integer_string_fails_parse() {
            let expected = Err("Not an integer: abc".to_string());
            assert_eq!(validate_idiomatic("abc"), expected);
            assert_eq!(validate_explicit("abc"), expected);
            assert_eq!(validate_question_mark("abc"), expected);
        }
    
        #[test]
        fn test_positive_odd_fails_check_even() {
            let expected = Err("Must be even".to_string());
            assert_eq!(validate_idiomatic("7"), expected);
            assert_eq!(validate_explicit("7"), expected);
            assert_eq!(validate_question_mark("7"), expected);
        }
    
        #[test]
        fn test_zero_fails_check_positive() {
            // 0 is not > 0
            let expected = Err("Must be positive".to_string());
            assert_eq!(validate_idiomatic("0"), expected);
            assert_eq!(validate_explicit("0"), expected);
            assert_eq!(validate_question_mark("0"), expected);
        }
    
        #[test]
        fn test_parse_int_valid() {
            assert_eq!(parse_int("100"), Ok(100));
            assert_eq!(parse_int("-5"), Ok(-5));
            assert_eq!(parse_int("0"), Ok(0));
        }
    
        #[test]
        fn test_parse_int_invalid() {
            assert!(parse_int("abc").is_err());
            assert!(parse_int("1.5").is_err());
            assert!(parse_int("").is_err());
        }
    
        #[test]
        fn test_error_stops_at_first_failure() {
            // "abc" fails parse_int — check_positive and check_even never run
            // confirmed by the error message being about parsing, not positivity/parity
            let result = validate_idiomatic("abc");
            assert!(result.unwrap_err().contains("Not an integer"));
        }
    }

    Deep Comparison

    OCaml vs Rust: Result Monad — Error Chaining

    Side-by-Side Code

    OCaml

    let ( >>= ) r f = match r with
      | Error _ as e -> e
      | Ok x -> f x
    
    let parse_int s =
      match int_of_string_opt s with
      | Some n -> Ok n
      | None -> Error ("Not an integer: " ^ s)
    
    let check_positive n =
      if n > 0 then Ok n else Error "Must be positive"
    
    let check_even n =
      if n mod 2 = 0 then Ok n else Error "Must be even"
    
    let validate s =
      parse_int s >>= check_positive >>= check_even
    

    Rust (idiomatic — and_then chain)

    pub fn parse_int(s: &str) -> Result<i64, String> {
        s.parse::<i64>().map_err(|_| format!("Not an integer: {s}"))
    }
    
    pub fn check_positive(n: i64) -> Result<i64, String> {
        if n > 0 { Ok(n) } else { Err("Must be positive".to_string()) }
    }
    
    pub fn check_even(n: i64) -> Result<i64, String> {
        if n % 2 == 0 { Ok(n) } else { Err("Must be even".to_string()) }
    }
    
    pub fn validate_idiomatic(s: &str) -> Result<i64, String> {
        parse_int(s).and_then(check_positive).and_then(check_even)
    }
    

    Rust (? operator — sequential style)

    pub fn validate_question_mark(s: &str) -> Result<i64, String> {
        let n = parse_int(s)?;
        let n = check_positive(n)?;
        check_even(n)
    }
    

    Rust (explicit bind — mirrors OCaml's >>= directly)

    fn bind<T, U, E>(r: Result<T, E>, f: impl FnOnce(T) -> Result<U, E>) -> Result<U, E> {
        match r {
            Err(e) => Err(e),
            Ok(x) => f(x),
        }
    }
    
    pub fn validate_explicit(s: &str) -> Result<i64, String> {
        bind(bind(parse_int(s), check_positive), check_even)
    }
    

    Type Signatures

    ConceptOCamlRust
    Result type('a, 'b) resultResult<T, E>
    Bind operatorval (>>=) : ('a,'e) result -> ('a -> ('b,'e) result) -> ('b,'e) resultfn and_then<U, F>(self, f: F) -> Result<U, E>
    Validate signatureval validate : string -> (int, string) resultfn validate(s: &str) -> Result<i64, String>
    Error conversion"Not an integer: " ^ s (string concat)format!("Not an integer: {s}") + map_err
    Short-circuitError _ as e -> e in >>=Implicit in and_then / early return via ?

    Key Insights

  • **and_then is >>=:** Rust's Result::and_then is the stdlib bind combinator — no custom operator needed. It unwraps Ok and passes the value to the next function, or passes Err through unchanged.
  • **? is do-notation sugar:** The ? operator desugars to a match on Result that early-returns Err. Sequential ? usage gives the same left-to-right chaining as >>= but looks like ordinary imperative code.
  • Error type uniformity: OCaml's polymorphic result lets you mix error types freely. Rust requires all steps in a chain to share the same E — here String. map_err is the idiomatic adapter when upstream errors differ.
  • First failure wins: In both languages the chain stops at the first Err/Error. parse_int "abc" never reaches check_positive or check_even; the error message reflects exactly which step failed.
  • Railway metaphor: Each validation function is a railway switch — the "success track" (Ok) continues forward; the "error track" (Err) bypasses all remaining steps and exits at the end. This pattern makes error handling compositional and centralized.
  • When to Use Each Style

    **Use and_then chain when:** The validators are already written as standalone functions and you want a point-free, functional pipeline that reads like a sentence.

    **Use ? operator when:** The validation logic is complex, needs intermediate let-bindings, or benefits from the familiar sequential look — particularly inside a function body with other logic.

    **Use explicit bind when:** You are teaching the monad concept or need to abstract over multiple monadic types (e.g., building a generic combinator library).

    Exercises

  • Extend the error chain to use a custom error enum with distinct variants for each failure mode, and map each step's error type using map_err.
  • Implement result_all — analogous to option_all — that collects a Vec<Result<T, E>> into Result<Vec<T>, E>, returning the first error encountered.
  • Combine Option and Result chaining: write a function that looks up a configuration key (returning Option), parses its value (returning Result), and applies a range check (returning Result), threading through a unified error type.
  • Open Source Repos