๐Ÿฆ€ 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

755: Testing Error Cases and Unwrap Discipline

Difficulty: 2 Level: Intermediate Test every error variant explicitly โ€” not just `is_err()`, but which error and what values it carries.

The Problem This Solves

Most developers test the happy path thoroughly and add one or two `assert!(result.is_err())` checks for errors. This leaves the error logic essentially untested. Did you return the right error? Does it contain the right position, the right value, the right message? You won't know until a user reports a confusing error message in production. Rich error types carry data. `ParseError::InvalidChar { ch: 'x', pos: 2 }` is far more useful than `ParseError::Invalid`. But only if you test that `ch` is `'x'` and `pos` is `2` โ€” not just that an error occurred. If you refactor the parser and the position becomes `3` instead of `2`, your tests should catch it. There's also `unwrap()` discipline to consider. Using `.unwrap()` in tests is fine when the value should be `Ok` โ€” a failing `.unwrap()` gives you a clear panic with the actual value. But use `.expect("descriptive message")` for better diagnostics. Avoid using `.unwrap()` in business logic; prefer `.expect()` or `?`.

The Intuition

In Python, you'd use `pytest.raises(ValueError) as exc_info` and then inspect `exc_info.value.args`. In Rust, you pattern-match on the error variant and destructure its fields โ€” the compiler ensures you check all the data the error carries. The `match` pattern `Err(ParseError::TooLong { len: 11, max: 10 }) => {}` reads naturally: "I expect a TooLong error where len is 11 and max is 10". If the implementation changes the max to 9, the test fails.

How It Works in Rust

// Rich error enum with data in each variant
#[derive(Debug, PartialEq, Clone)]
pub enum ParseError {
 Empty,
 TooLong { len: usize, max: usize },
 InvalidChar { ch: char, pos: usize },
 OutOfRange { value: i64, min: i64, max: i64 },
}

// Testing happy path โ€” use expect() for clear failure messages
#[test]
fn parse_valid_number() {
 let n = parse_positive("42").expect("'42' is a valid positive integer");
 assert_eq!(n, 42);
}

// Testing error variant and its fields
#[test]
fn parse_too_long_returns_correct_lengths() {
 let long = "1".repeat(11);
 match parse_positive(&long) {
     Err(ParseError::TooLong { len: 11, max: 10 }) => {}  // exactly right
     other => panic!("expected TooLong(11, 10), got {:?}", other),
 }
}

// Testing error position โ€” char at index 2 is invalid
#[test]
fn parse_invalid_char_reports_position() {
 match parse_positive("12x45") {
     Err(ParseError::InvalidChar { ch: 'x', pos: 2 }) => {}
     other => panic!("expected InvalidChar('x', 2), got {:?}", other),
 }
}

// Using `..` to match only fields you care about
#[test]
fn parse_zero_is_out_of_range() {
 match parse_positive("0") {
     Err(ParseError::OutOfRange { value: 0, .. }) => {}
     other => panic!("expected OutOfRange(0), got {:?}", other),
 }
}

// assert_eq! works when error type derives PartialEq
#[test]
fn empty_input_is_empty_error() {
 assert_eq!(parse_positive(""), Err(ParseError::Empty));
}

// #[should_panic] for testing that unwrap panics
#[test]
#[should_panic(expected = "called `Result::unwrap()`")]
fn unwrap_on_err_panics() {
 parse_positive("").unwrap();
}

// Graceful defaults in production code
#[test]
fn unwrap_or_else_for_defaults() {
 let n = parse_positive("bad").unwrap_or_else(|_| 0);
 assert_eq!(n, 0);
}
Key points:

What This Unlocks

Key Differences

ConceptOCamlRust
Error typePolymorphic variants or exceptions`enum` with data per variant
Matching errors in tests`match result withError X -> ...``match result { Err(MyError::X { field }) => ... }`
Equality check`(=)` operator or customDerive `PartialEq` on error enum
Expected panic`assert_raises``#[should_panic(expected = "...")]`
Unwrap in tests`Option.get` / `Result.get_ok``.unwrap()` or `.expect("msg")`
Partial field matchN/A`..` pattern ignores remaining fields
/// 755: Testing Error Cases and Unwrap Discipline

// โ”€โ”€ Error types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, PartialEq, Clone)]
pub enum ParseError {
    Empty,
    TooLong { len: usize, max: usize },
    InvalidChar { ch: char, pos: usize },
    OutOfRange { value: i64, min: i64, max: i64 },
}

impl std::fmt::Display for ParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ParseError::Empty                    => write!(f, "input is empty"),
            ParseError::TooLong { len, max }     => write!(f, "too long: {} > {}", len, max),
            ParseError::InvalidChar { ch, pos }  => write!(f, "invalid char {:?} at {}", ch, pos),
            ParseError::OutOfRange { value, min, max } =>
                write!(f, "{} out of range [{}, {}]", value, min, max),
        }
    }
}

// โ”€โ”€ Function under test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub fn parse_positive(s: &str) -> Result<u32, ParseError> {
    if s.is_empty() {
        return Err(ParseError::Empty);
    }
    if s.len() > 10 {
        return Err(ParseError::TooLong { len: s.len(), max: 10 });
    }
    for (pos, ch) in s.char_indices() {
        if !ch.is_ascii_digit() {
            return Err(ParseError::InvalidChar { ch, pos });
        }
    }
    let n: u64 = s.parse().unwrap(); // safe: all digits verified
    if n == 0 || n > u32::MAX as u64 {
        return Err(ParseError::OutOfRange { value: n as i64, min: 1, max: u32::MAX as i64 });
    }
    Ok(n as u32)
}

pub fn divide(a: i64, b: i64) -> Result<i64, &'static str> {
    if b == 0 { Err("cannot divide by zero") } else { Ok(a / b) }
}

pub fn head<T: Clone>(v: &[T]) -> Result<T, &'static str> {
    v.first().cloned().ok_or("slice is empty")
}

fn main() {
    println!("{:?}", parse_positive("42"));
    println!("{:?}", parse_positive(""));
    println!("{:?}", parse_positive("12345678901"));
    println!("{:?}", parse_positive("12x45"));
    println!("{:?}", divide(10, 3));
    println!("{:?}", divide(10, 0));
}

// โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

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

    // โ”€โ”€ Happy path โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn parse_positive_valid() {
        // Use expect() โ€” gives context on failure
        let n = parse_positive("42").expect("'42' is a valid positive integer");
        assert_eq!(n, 42);
    }

    #[test]
    fn parse_positive_boundary_values() {
        assert_eq!(parse_positive("1").expect("1 is valid"), 1);
        // u32::MAX = 4294967295 (10 chars โ€” valid)
        let max_str = u32::MAX.to_string();
        assert_eq!(parse_positive(&max_str).expect("u32::MAX is valid"), u32::MAX);
    }

    // โ”€โ”€ Error paths โ€” variant-specific โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn parse_empty_returns_empty_error() {
        assert_eq!(parse_positive(""), Err(ParseError::Empty));
    }

    #[test]
    fn parse_too_long_returns_correct_lengths() {
        let long = "1".repeat(11);
        match parse_positive(&long) {
            Err(ParseError::TooLong { len: 11, max: 10 }) => {}
            other => panic!("expected TooLong, got {:?}", other),
        }
    }

    #[test]
    fn parse_invalid_char_reports_position() {
        match parse_positive("12x45") {
            Err(ParseError::InvalidChar { ch: 'x', pos: 2 }) => {}
            other => panic!("expected InvalidChar('x',2), got {:?}", other),
        }
    }

    #[test]
    fn parse_zero_is_out_of_range() {
        match parse_positive("0") {
            Err(ParseError::OutOfRange { value: 0, .. }) => {}
            other => panic!("expected OutOfRange(0), got {:?}", other),
        }
    }

    // โ”€โ”€ is_err / is_ok checks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn parse_non_digit_is_err() {
        assert!(parse_positive("abc").is_err());
        assert!(parse_positive("-1").is_err());
        assert!(parse_positive("1.5").is_err());
    }

    // โ”€โ”€ should_panic โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    #[should_panic(expected = "called `Result::unwrap()`")]
    fn unwrap_on_err_panics() {
        parse_positive("").unwrap();
    }

    #[test]
    #[should_panic(expected = "parse_positive should succeed")]
    fn expect_gives_context_on_failure() {
        parse_positive("not-a-number")
            .expect("parse_positive should succeed");
    }

    // โ”€โ”€ unwrap_or_else discipline โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

    #[test]
    fn use_unwrap_or_else_for_defaults() {
        let n = parse_positive("bad").unwrap_or_else(|_| 0);
        assert_eq!(n, 0);
    }

    #[test]
    fn divide_by_zero_is_err() {
        assert!(divide(10, 0).is_err());
        assert_eq!(divide(10, 0), Err("cannot divide by zero"));
    }

    #[test]
    fn head_empty_is_err() {
        let v: Vec<i32> = vec![];
        assert_eq!(head(&v), Err("slice is empty"));
    }

    #[test]
    fn head_non_empty_is_ok() {
        let v = vec![42, 1, 2];
        assert_eq!(head(&v).expect("non-empty slice has a head"), 42);
    }
}
(* 755: Testing Error Paths โ€” OCaml *)

type parse_error =
  | Empty
  | InvalidChar of char
  | TooLong
  | OutOfRange of int

let parse_positive (s : string) : (int, parse_error) result =
  if String.length s = 0 then Error Empty
  else if String.length s > 10 then Error TooLong
  else
    match String.to_seq s |> Seq.find (fun c -> c < '0' || c > '9') with
    | Some c -> Error (InvalidChar c)
    | None ->
      let n = int_of_string s in
      if n <= 0 then Error (OutOfRange n)
      else Ok n

(* Test helpers for errors *)
let assert_error ?(expected=None) result =
  match result, expected with
  | Error _, None -> ()
  | Error e, Some exp ->
    if e <> exp then
      failwith (Printf.sprintf "wrong error variant")
  | Ok _, _ -> failwith "expected Error, got Ok"

let () =
  (* Happy path *)
  assert (parse_positive "42" = Ok 42);

  (* Error paths *)
  assert_error (parse_positive "");
  assert_error ~expected:(Some Empty) (parse_positive "");
  assert_error ~expected:(Some TooLong) (parse_positive "12345678901");
  assert_error (parse_positive "12a45");
  assert_error ~expected:(Some (OutOfRange (-5))) (parse_positive "-5");

  (* expect-style: assert with message *)
  let result = parse_positive "100" in
  let n = match result with
    | Ok n -> n
    | Error _ -> failwith "parse_positive '100' should succeed"
  in
  assert (n = 100);

  Printf.printf "Error path tests passed!\n"