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

1003: Custom Error Types

Difficulty: Intermediate Category: Error Handling Concept: Defining custom error types with `Display` and `Error` trait implementations Key Insight: Rust's error types are regular enums with trait implementations, giving you pattern matching on errors โ€” unlike OCaml's exceptions which bypass the type system.
// 1003: Custom Error Types
// Custom error type with Display + Error impl

use std::fmt;

// Approach 1: Simple error enum with Display
#[derive(Debug, PartialEq)]
enum ValidationError {
    NegativeAge(i32),
    UnreasonableAge(i32),
    EmptyName,
}

impl fmt::Display for ValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ValidationError::NegativeAge(n) => write!(f, "negative age: {}", n),
            ValidationError::UnreasonableAge(n) => write!(f, "unreasonable age: {}", n),
            ValidationError::EmptyName => write!(f, "name cannot be empty"),
        }
    }
}

impl std::error::Error for ValidationError {}

fn validate_age(age: i32) -> Result<i32, ValidationError> {
    if age < 0 {
        Err(ValidationError::NegativeAge(age))
    } else if age > 150 {
        Err(ValidationError::UnreasonableAge(age))
    } else {
        Ok(age)
    }
}

fn validate_name(name: &str) -> Result<&str, ValidationError> {
    if name.is_empty() {
        Err(ValidationError::EmptyName)
    } else {
        Ok(name)
    }
}

// Approach 2: Error with structured context
#[derive(Debug)]
struct DetailedError {
    field: String,
    message: String,
}

impl fmt::Display for DetailedError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "field '{}': {}", self.field, self.message)
    }
}

impl std::error::Error for DetailedError {}

fn validate_field(field: &str, value: &str) -> Result<(), DetailedError> {
    if value.is_empty() {
        Err(DetailedError {
            field: field.to_string(),
            message: "cannot be empty".to_string(),
        })
    } else {
        Ok(())
    }
}

fn main() {
    // Demo usage
    match validate_age(-5) {
        Ok(age) => println!("Valid age: {}", age),
        Err(e) => println!("Error: {}", e),
    }

    match validate_field("email", "") {
        Ok(()) => println!("Valid field"),
        Err(e) => println!("Error: {}", e),
    }

    println!("Run `cargo test` to verify all examples.");
}

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

    #[test]
    fn test_valid_age() {
        assert_eq!(validate_age(25), Ok(25));
    }

    #[test]
    fn test_negative_age() {
        assert_eq!(validate_age(-5), Err(ValidationError::NegativeAge(-5)));
    }

    #[test]
    fn test_unreasonable_age() {
        assert_eq!(validate_age(200), Err(ValidationError::UnreasonableAge(200)));
    }

    #[test]
    fn test_display_impl() {
        let err = ValidationError::NegativeAge(-1);
        assert_eq!(err.to_string(), "negative age: -1");

        let err = ValidationError::EmptyName;
        assert_eq!(err.to_string(), "name cannot be empty");
    }

    #[test]
    fn test_error_trait() {
        let err: Box<dyn std::error::Error> = Box::new(ValidationError::EmptyName);
        assert_eq!(err.to_string(), "name cannot be empty");
    }

    #[test]
    fn test_validate_name() {
        assert_eq!(validate_name("Alice"), Ok("Alice"));
        assert_eq!(validate_name(""), Err(ValidationError::EmptyName));
    }

    #[test]
    fn test_detailed_error() {
        let result = validate_field("email", "");
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().to_string(), "field 'email': cannot be empty");
    }
}
(* 1003: Custom Error Types *)
(* OCaml exception vs Rust enum-based errors *)

(* Approach 1: Built-in exceptions *)
exception Invalid_age of string
exception Parse_error of string

let validate_age_exn age =
  if age < 0 then raise (Invalid_age "age cannot be negative")
  else if age > 150 then raise (Invalid_age "age unreasonably large")
  else age

let test_approach1 () =
  assert (validate_age_exn 25 = 25);
  (try
     let _ = validate_age_exn (-1) in
     assert false
   with Invalid_age msg -> assert (msg = "age cannot be negative"));
  (try
     let _ = validate_age_exn 200 in
     assert false
   with Invalid_age msg -> assert (msg = "age unreasonably large"));
  Printf.printf "  Approach 1 (exceptions): passed\n"

(* Approach 2: Result type with custom error variant *)
type validation_error =
  | NegativeAge of int
  | UnreasonableAge of int
  | EmptyName

let string_of_validation_error = function
  | NegativeAge n -> Printf.sprintf "negative age: %d" n
  | UnreasonableAge n -> Printf.sprintf "unreasonable age: %d" n
  | EmptyName -> "name cannot be empty"

let validate_age age =
  if age < 0 then Error (NegativeAge age)
  else if age > 150 then Error (UnreasonableAge age)
  else Ok age

let validate_name name =
  if String.length name = 0 then Error EmptyName
  else Ok name

let test_approach2 () =
  assert (validate_age 25 = Ok 25);
  assert (validate_age (-5) = Error (NegativeAge (-5)));
  assert (validate_age 200 = Error (UnreasonableAge 200));
  assert (validate_name "Alice" = Ok "Alice");
  assert (validate_name "" = Error EmptyName);
  (match validate_age (-1) with
   | Error e -> assert (string_of_validation_error e = "negative age: -1")
   | Ok _ -> assert false);
  Printf.printf "  Approach 2 (result type): passed\n"

(* Approach 3: Polymorphic variants for lightweight errors *)
let validate_age_poly age =
  if age < 0 then Error (`NegativeAge age)
  else if age > 150 then Error (`UnreasonableAge age)
  else Ok age

let test_approach3 () =
  assert (validate_age_poly 30 = Ok 30);
  (match validate_age_poly (-1) with
   | Error (`NegativeAge n) -> assert (n = -1)
   | _ -> assert false);
  Printf.printf "  Approach 3 (polymorphic variants): passed\n"

let () =
  Printf.printf "Testing custom error types:\n";
  test_approach1 ();
  test_approach2 ();
  test_approach3 ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Custom Error Types โ€” Comparison

Core Insight

OCaml exceptions are dynamic and bypass the type checker; Rust errors are typed enums that the compiler tracks through `Result<T, E>`.

OCaml Approach

  • `exception` declarations create runtime-only error types
  • Exceptions don't appear in function signatures
  • Callers have no compile-time indication a function can fail
  • Polymorphic variants offer a typed alternative but lack the `Error` trait ecosystem

Rust Approach

  • Error types are regular enums implementing `Display` and `Error`
  • `Result<T, E>` in the return type makes fallibility explicit
  • Pattern matching on error variants is exhaustive
  • The `Error` trait enables interop with `Box<dyn Error>` and error-handling crates

Comparison Table

AspectOCamlRust
Error declaration`exception Foo of string``enum MyError { Foo(String) }`
Type visibilityNot in signatureIn `Result<T, E>` return type
Pattern matching`try ... with``match result { Ok/Err }`
ExhaustivenessNo (catch-all needed)Yes (compiler enforced)
DisplayManual `string_of_*``impl Display` trait
ComposabilityLimited`Error` trait + `From` + `?`