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

299: Adding Context to Errors

Difficulty: 3 Level: Advanced Attach "where and why" information to errors without losing the original cause.

The Problem This Solves

A production error log shows: `"not found"`. Which file? Which operation? There's no way to know without adding context at the point of failure โ€” but if you just convert the error to a string, you lose the original structured error that callers might want to inspect programmatically. Context wrapping solves this. Instead of swallowing the original error, you wrap it: the outer error carries a human-readable message explaining what you were trying to do, and the inner error (accessible via `source()`) preserves the original cause. Callers get both: a readable message and a traversable causal chain. This is what `anyhow::Context` does, and what production code needs. A bare `file not found` is useless. `loading config from '/etc/app/config.toml': file not found` tells an engineer exactly what to fix.

The Intuition

Context wrapping is a linked-list prepend: add a new "what I was doing" node to the front of the error chain, while preserving the original error as `source()`.

How It Works in Rust

// A generic context wrapper โ€” holds a message and the original error
#[derive(Debug)]
struct Context<E> {
 message: String,
 source: E,
}

impl<E: fmt::Display> fmt::Display for Context<E> {
 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     write!(f, "{}", self.message)  // show the context message, not the inner error
 }
}

impl<E: Error + 'static> Error for Context<E> {
 fn source(&self) -> Option<&(dyn Error + 'static)> {
     Some(&self.source)  // the original error is still accessible
 }
}

// Extension trait: adds .context("...") to any Result
trait WithContext<T, E> {
 fn context(self, msg: &str) -> Result<T, Context<E>>;
}

impl<T, E: Error> WithContext<T, E> for Result<T, E> {
 fn context(self, msg: &str) -> Result<T, Context<E>> {
     self.map_err(|e| Context { message: msg.to_string(), source: e })
 }
}

// Usage: context at every layer adds "what you were doing"
fn load_config(path: &str) -> Result<String, Context<IoError>> {
 read_file(path).context(&format!("loading config from '{}'", path))
}
Walk the chain: `println!("{}", e)` prints the context message. `e.source()` gives the original `IoError`. A logging framework can walk `source()` repeatedly to print the full causal history.

What This Unlocks

Key Differences

ConceptOCamlRust
Context wrappingManual tuple type or string annotationWrapper struct implementing `Error::source()`
Chain traversalManual recursion over cause fieldStandard `source()` linked list
ErgonomicsVerbose at every call site`.context()` extension method on `Result`
Message vs causeCombined or separateSeparate: `Display` = message, `source()` = cause
//! 299. Adding context to errors
//!
//! Context wrapping adds layers of "where/why" around errors via the `source()` chain.

use std::error::Error;
use std::fmt;

/// Generic context wrapper
#[derive(Debug)]
pub struct Context<E> {
    message: String,
    source: E,
}

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

impl<E: Error + 'static> Error for Context<E> {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.source)
    }
}

/// Extension trait to add context to any Result
trait WithContext<T, E> {
    fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, Context<E>>;
    fn context(self, msg: &str) -> Result<T, Context<E>>;
}

impl<T, E: Error> WithContext<T, E> for Result<T, E> {
    fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, Context<E>> {
        self.map_err(|e| Context { message: f(), source: e })
    }
    fn context(self, msg: &str) -> Result<T, Context<E>> {
        self.with_context(|| msg.to_string())
    }
}

#[derive(Debug)]
struct IoError(String);
impl fmt::Display for IoError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } }
impl Error for IoError {}

fn read_file(path: &str) -> Result<String, IoError> {
    if path.ends_with(".missing") { Err(IoError(format!("{}: not found", path))) }
    else { Ok(format!("contents of {}", path)) }
}

fn load_config(path: &str) -> Result<String, Context<IoError>> {
    read_file(path).context(&format!("loading config from '{}'", path))
}

fn print_chain(e: &dyn Error) {
    println!("  Error: {}", e);
    let mut cause = e.source();
    while let Some(c) = cause {
        println!("  Caused by: {}", c);
        cause = c.source();
    }
}

fn main() {
    match load_config("app.missing") {
        Ok(c) => println!("Config: {}", c),
        Err(ref e) => print_chain(e),
    }
    match load_config("app.toml") {
        Ok(c) => println!("Config: {}", c),
        Err(ref e) => print_chain(e),
    }
}

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

    #[test]
    fn test_context_ok() {
        let result = read_file("test.toml").context("reading test file");
        assert!(result.is_ok());
    }

    #[test]
    fn test_context_preserves_source() {
        let result = load_config("x.missing");
        assert!(result.is_err());
        let e = result.unwrap_err();
        assert!(e.source().is_some());
        assert!(format!("{}", e).contains("loading config"));
    }

    #[test]
    fn test_context_message() {
        let result: Result<(), IoError> = Err(IoError("fail".to_string()));
        let ctx = result.context("doing something");
        assert!(ctx.is_err());
        assert!(format!("{}", ctx.unwrap_err()).contains("doing something"));
    }
}
(* 299. Adding context to errors - OCaml *)

type 'e contexted = { context: string; cause: 'e }

let with_context ctx = function
  | Error e -> Error { context = ctx; cause = e }
  | Ok _ as r -> r

let () =
  let read_file name =
    if name = "missing.txt" then Error "file not found"
    else Ok "file contents"
  in
  let load_config path =
    read_file path
    |> with_context ("loading config from " ^ path)
  in
  let init_app () =
    load_config "missing.txt"
    |> (function
      | Error { context; cause } ->
        Error { context = "initializing app"; cause = context ^ ": " ^ cause }
      | Ok _ as r -> r)
  in
  match init_app () with
  | Ok _ -> print_endline "App started"
  | Error { context; cause } ->
    Printf.printf "Error [%s]: %s\n" context cause