Result Monad — Error Chaining
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 (>>=)? operator desugars to early-return on Err, giving monadic sequencing with imperative syntaxmap_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
>>=; Rust uses the .and_then() method or the ? operator — no operator overloading needed.map_err to convert parse errors into the uniform String error type.? operator provides do-notation-style sequencing without a monad typeclass — each ? is an explicit bind step.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"));
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Result type | ('a, 'b) result | Result<T, E> |
| Bind operator | val (>>=) : ('a,'e) result -> ('a -> ('b,'e) result) -> ('b,'e) result | fn and_then<U, F>(self, f: F) -> Result<U, E> |
| Validate signature | val validate : string -> (int, string) result | fn validate(s: &str) -> Result<i64, String> |
| Error conversion | "Not an integer: " ^ s (string concat) | format!("Not an integer: {s}") + map_err |
| Short-circuit | Error _ 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.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.Err/Error. parse_int "abc" never reaches check_positive or check_even; the error message reflects exactly which step failed.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
map_err.result_all — analogous to option_all — that collects a Vec<Result<T, E>> into Result<Vec<T>, E>, returning the first error encountered.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.