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

1016: Error Context

Difficulty: Intermediate Category: Error Handling Concept: Adding context and backtrace information to errors as they propagate through layers Key Insight: A `Context` extension trait on `Result` mimics what `anyhow::Context` provides โ€” each layer adds a breadcrumb, building a human-readable error chain without losing the original cause.
// 1016: Error Context
// Add context/backtrace to errors manually using wrapper structs

use std::fmt;

// Approach 1: Context wrapper struct
#[derive(Debug)]
struct ErrorWithContext {
    message: String,
    context: Vec<String>,
}

impl ErrorWithContext {
    fn new(message: impl Into<String>) -> Self {
        ErrorWithContext {
            message: message.into(),
            context: Vec::new(),
        }
    }

    fn with_context(mut self, ctx: impl Into<String>) -> Self {
        self.context.push(ctx.into());
        self
    }
}

impl fmt::Display for ErrorWithContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.context.is_empty() {
            write!(f, "{}", self.message)
        } else {
            let chain: Vec<&str> = self.context.iter().rev().map(|s| s.as_str()).collect();
            write!(f, "{}: {}", chain.join(" -> "), self.message)
        }
    }
}

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

// Extension trait for adding context to any Result
trait Context<T> {
    fn context(self, ctx: impl Into<String>) -> Result<T, ErrorWithContext>;
    fn with_context(self, f: impl FnOnce() -> String) -> Result<T, ErrorWithContext>;
}

impl<T> Context<T> for Result<T, ErrorWithContext> {
    fn context(self, ctx: impl Into<String>) -> Result<T, ErrorWithContext> {
        self.map_err(|e| e.with_context(ctx))
    }
    fn with_context(self, f: impl FnOnce() -> String) -> Result<T, ErrorWithContext> {
        self.map_err(|e| e.with_context(f()))
    }
}

// Low-level functions
fn read_file(path: &str) -> Result<String, ErrorWithContext> {
    if path == "/missing" {
        Err(ErrorWithContext::new("file not found"))
    } else {
        Ok("42".into())
    }
}

fn parse_config(content: &str) -> Result<i64, ErrorWithContext> {
    content
        .parse::<i64>()
        .map_err(|e| ErrorWithContext::new(format!("invalid number: {}", e)))
}

// Approach 2: Chain contexts through layers
fn load_setting(path: &str) -> Result<i64, ErrorWithContext> {
    let content = read_file(path).context("reading config")?;
    let value = parse_config(&content).context("parsing config")?;
    Ok(value)
}

fn init_system(path: &str) -> Result<i64, ErrorWithContext> {
    load_setting(path).context("system init")
}

fn main() {
    match init_system("/missing") {
        Ok(v) => println!("Value: {}", v),
        Err(e) => println!("Error: {}", e),
        // Prints: "system init -> reading config: file not found"
    }
    println!("Run `cargo test` to verify all examples.");
}

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

    #[test]
    fn test_no_context() {
        let err = ErrorWithContext::new("oops");
        assert_eq!(err.to_string(), "oops");
    }

    #[test]
    fn test_single_context() {
        let err = ErrorWithContext::new("oops").with_context("loading");
        assert_eq!(err.to_string(), "loading: oops");
    }

    #[test]
    fn test_nested_context() {
        let result = init_system("/missing");
        let err = result.unwrap_err();
        assert_eq!(err.context.len(), 2);
        assert!(err.to_string().contains("system init"));
        assert!(err.to_string().contains("reading config"));
        assert!(err.to_string().contains("file not found"));
    }

    #[test]
    fn test_success_passes_through() {
        assert_eq!(init_system("/ok").unwrap(), 42);
    }

    #[test]
    fn test_context_trait() {
        let result: Result<i64, ErrorWithContext> = Err(ErrorWithContext::new("base"));
        let result = result.context("layer1");
        let result = result.map_err(|e| e.with_context("layer2"));
        let err = result.unwrap_err();
        assert_eq!(err.context.len(), 2);
    }

    #[test]
    fn test_lazy_context() {
        let result: Result<i64, ErrorWithContext> = Err(ErrorWithContext::new("base"));
        let result = result.with_context(|| format!("dynamic context {}", 42));
        let err = result.unwrap_err();
        assert!(err.to_string().contains("dynamic context 42"));
    }
}
(* 1016: Error Context *)
(* Adding context/backtrace to errors manually *)

type error_with_context = {
  message: string;
  context: string list;  (* stack of context strings *)
}

let make_error msg = { message = msg; context = [] }

let add_context ctx err =
  { err with context = ctx :: err.context }

let display_error err =
  let chain = String.concat " -> " (List.rev err.context) in
  if chain = "" then err.message
  else Printf.sprintf "%s: %s" chain err.message

(* Approach 1: Manual context threading *)
let read_file path =
  if path = "/missing" then Error (make_error "file not found")
  else Ok "42"

let parse_config content =
  match int_of_string_opt content with
  | None -> Error (make_error "invalid number")
  | Some n -> Ok n

let load_setting path =
  match read_file path with
  | Error e -> Error (add_context "reading config" e)
  | Ok content ->
    match parse_config content with
    | Error e -> Error (add_context "parsing config" e)
    | Ok n -> Ok n

let init_system path =
  match load_setting path with
  | Error e -> Error (add_context "system init" e)
  | Ok n -> Ok n

(* Approach 2: Result pipe with context *)
let ( >>| ) r ctx =
  match r with
  | Ok v -> Ok v
  | Error e -> Error (add_context ctx e)

let load_setting_pipe path =
  (read_file path >>| "reading config")
  |> Result.bind (fun content ->
       parse_config content >>| "parsing config")

let test_manual () =
  (match init_system "/missing" with
   | Error e ->
     let msg = display_error e in
     assert (String.length msg > 0);
     assert (List.length e.context = 2)
   | Ok _ -> assert false);
  assert (init_system "/ok" = Ok 42);
  Printf.printf "  Approach 1 (manual context): passed\n"

let test_pipe () =
  (match load_setting_pipe "/missing" with
   | Error e -> assert (List.length e.context = 1)
   | Ok _ -> assert false);
  assert (load_setting_pipe "/ok" = Ok 42);
  Printf.printf "  Approach 2 (pipe with context): passed\n"

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

๐Ÿ“Š Detailed Comparison

Error Context โ€” Comparison

Core Insight

Raw errors like "file not found" are useless without knowing which file, in which operation, at which layer. Context wrapping builds a breadcrumb trail.

OCaml Approach

  • Record with `context: string list` accumulates breadcrumbs
  • Custom `>>|` operator adds context in pipelines
  • No standard library support โ€” each project rolls its own

Rust Approach

  • Wrapper struct with `Vec<String>` context chain
  • Extension trait `Context` on `Result` โ€” `.context("msg")?`
  • Lazy variant: `.with_context(|| format!(...))?`
  • Real-world: `anyhow::Context` trait does exactly this

Comparison Table

AspectOCamlRust
Context accumulator`string list` field`Vec<String>` field
Adding contextCustom `>>` operator`.context()` trait method
Lazy contextThunk `fun () -> ...`Closure `\\format!(...)`
Standard libraryNoNo (but `anyhow` is de facto standard)
Display formatManual `String.concat`Custom `Display` impl