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

1005: Error Chaining

Difficulty: Intermediate Category: Error Handling Concept: Adding context to errors as they propagate through layers using `map_err` Key Insight: `map_err` transforms the error type in-flight, letting each layer add its own context โ€” like building a stack trace manually but with structured types.
// 1005: Error Chaining
// Chain errors with context using map_err

use std::fmt;

#[derive(Debug)]
enum IoError {
    NotFound,
    Corrupted(String),
}

impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            IoError::NotFound => write!(f, "not found"),
            IoError::Corrupted(s) => write!(f, "corrupted: {}", s),
        }
    }
}

#[derive(Debug)]
struct AppError {
    context: String,
    source: IoError,
}

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

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        None // IoError doesn't impl Error in this example for simplicity
    }
}

// Low-level function returning raw error
fn read_file(path: &str) -> Result<String, IoError> {
    match path {
        "/missing" => Err(IoError::NotFound),
        "/bad" => Err(IoError::Corrupted("invalid utf-8".into())),
        _ => Ok("data".into()),
    }
}

// Approach 1: map_err to add context
fn load_config(path: &str) -> Result<String, AppError> {
    read_file(path).map_err(|e| AppError {
        context: format!("loading {}", path),
        source: e,
    })
}

// Approach 2: Generic context extension trait
trait WithContext<T> {
    fn with_context(self, ctx: impl FnOnce() -> String) -> Result<T, AppError>;
}

impl<T> WithContext<T> for Result<T, IoError> {
    fn with_context(self, ctx: impl FnOnce() -> String) -> Result<T, AppError> {
        self.map_err(|e| AppError {
            context: ctx(),
            source: e,
        })
    }
}

fn load_config_ext(path: &str) -> Result<String, AppError> {
    read_file(path).with_context(|| format!("loading {}", path))
}

fn main() {
    match load_config("/missing") {
        Ok(v) => println!("Got: {}", v),
        Err(e) => println!("Error: {}", e),
    }
    println!("Run `cargo test` to verify all examples.");
}

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

    #[test]
    fn test_success() {
        assert_eq!(load_config("/ok").unwrap(), "data");
    }

    #[test]
    fn test_map_err_context() {
        let err = load_config("/missing").unwrap_err();
        assert_eq!(err.context, "loading /missing");
        assert!(matches!(err.source, IoError::NotFound));
    }

    #[test]
    fn test_corrupted_context() {
        let err = load_config("/bad").unwrap_err();
        assert!(err.to_string().contains("corrupted"));
        assert!(err.to_string().contains("loading /bad"));
    }

    #[test]
    fn test_extension_trait() {
        assert_eq!(load_config_ext("/ok").unwrap(), "data");
        let err = load_config_ext("/missing").unwrap_err();
        assert_eq!(err.context, "loading /missing");
    }

    #[test]
    fn test_display_format() {
        let err = load_config("/missing").unwrap_err();
        assert_eq!(err.to_string(), "loading /missing: not found");
    }
}
(* 1005: Error Chaining *)
(* Adding context to errors as they propagate *)

type inner_error = NotFound | Corrupted of string
type app_error = { context: string; cause: inner_error }

let string_of_inner = function
  | NotFound -> "not found"
  | Corrupted s -> Printf.sprintf "corrupted: %s" s

let string_of_app_error e =
  Printf.sprintf "%s: %s" e.context (string_of_inner e.cause)

(* Approach 1: Manual context wrapping *)
let read_file path =
  if path = "/missing" then Error NotFound
  else if path = "/bad" then Error (Corrupted "invalid utf-8")
  else Ok "data"

let load_with_context path =
  match read_file path with
  | Error e -> Error { context = Printf.sprintf "loading %s" path; cause = e }
  | Ok v -> Ok v

(* Approach 2: Generic context helper (like Rust's map_err) *)
let with_context ctx result =
  match result with
  | Error e -> Error { context = ctx; cause = e }
  | Ok v -> Ok v

let load_with_helper path =
  read_file path |> with_context (Printf.sprintf "loading %s" path)

let test_manual () =
  (match load_with_context "/missing" with
   | Error e ->
     assert (e.context = "loading /missing");
     assert (e.cause = NotFound)
   | Ok _ -> assert false);
  assert (load_with_context "/ok" = Ok "data");
  Printf.printf "  Approach 1 (manual context): passed\n"

let test_helper () =
  (match load_with_helper "/bad" with
   | Error e ->
     assert (String.length (string_of_app_error e) > 0);
     assert (e.cause = Corrupted "invalid utf-8")
   | Ok _ -> assert false);
  assert (load_with_helper "/ok" = Ok "data");
  Printf.printf "  Approach 2 (context helper): passed\n"

let () =
  Printf.printf "Testing error chaining:\n";
  test_manual ();
  test_helper ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Error Chaining โ€” Comparison

Core Insight

Both languages can wrap errors with context, but Rust's `map_err` and extension traits make it idiomatic and composable at the type level.

OCaml Approach

  • Wrap errors in records or variant constructors manually
  • Can write `with_context` helpers that pattern-match on `Result`
  • No standard trait system for error chaining
  • Context is ad-hoc โ€” each project invents its own pattern

Rust Approach

  • `map_err(|e| ...)` transforms errors inline during propagation
  • Extension traits like `WithContext` mimic what `anyhow` provides
  • The `Error::source()` method creates a standard chain
  • Pattern is so common that crates like `anyhow` and `thiserror` standardize it

Comparison Table

AspectOCamlRust
Context additionManual record wrapping`map_err` / extension trait
Inline ergonomicsVerbose matchFluent `.map_err(...)`
Error chainCustom nesting`Error::source()` standard
Ecosystem supportAd-hoc`anyhow`, `thiserror` crates
Lazy contextClosure in helper`FnOnce` in extension trait