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

1026: Custom Error Display

Difficulty: Intermediate Category: Error Handling Concept: Custom `Display` implementations for nested errors with `Error::source()` chain walking Key Insight: Each error's `Display` describes only its own layer; `source()` links to the cause. Walking the chain produces a full trace โ€” like a stack trace but for error semantics, not call sites.
// 1026: Custom Display for Nested Errors with Source Chain
// Walking the Error::source() chain for human-readable output

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

// Inner error (root cause)
#[derive(Debug)]
enum IoError {
    FileNotFound(String),
    PermissionDenied(String),
}

impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            IoError::FileNotFound(p) => write!(f, "file not found: {}", p),
            IoError::PermissionDenied(p) => write!(f, "permission denied: {}", p),
        }
    }
}
impl Error for IoError {}

// Middle error (wraps inner)
#[derive(Debug)]
struct ConfigError {
    operation: String,
    source: IoError,
}

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

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

// Outer error (wraps middle)
#[derive(Debug)]
struct AppError {
    module_name: String,
    source: ConfigError,
}

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

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

// Approach 1: Walk the source chain
fn display_error_chain(err: &dyn Error) -> String {
    let mut chain = Vec::new();
    let mut current: Option<&dyn Error> = Some(err);
    let mut depth = 0;

    while let Some(e) = current {
        let prefix = if depth == 0 { "Error" } else { "Caused by" };
        let indent = "  ".repeat(depth);
        chain.push(format!("{}{}: {}", indent, prefix, e));
        current = e.source();
        depth += 1;
    }

    chain.join("\n")
}

// Approach 2: Collect all error messages into a vec
fn error_sources(err: &dyn Error) -> Vec<String> {
    let mut sources = vec![err.to_string()];
    let mut current = err.source();
    while let Some(e) = current {
        sources.push(e.to_string());
        current = e.source();
    }
    sources
}

// Approach 3: Single-line display with arrows
fn display_inline(err: &dyn Error) -> String {
    error_sources(err).join(" -> ")
}

fn make_error() -> AppError {
    AppError {
        module_name: "config".into(),
        source: ConfigError {
            operation: "reading settings".into(),
            source: IoError::FileNotFound("/etc/app.conf".into()),
        },
    }
}

fn main() {
    let err = make_error();
    println!("{}", display_error_chain(&err));
    println!();
    println!("Inline: {}", display_inline(&err));
    println!("Run `cargo test` to verify all examples.");
}

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

    #[test]
    fn test_display_chain() {
        let err = make_error();
        let chain = display_error_chain(&err);
        assert!(chain.contains("Error:"));
        assert!(chain.contains("Caused by:"));
        assert!(chain.contains("file not found"));
        let lines: Vec<&str> = chain.lines().collect();
        assert_eq!(lines.len(), 3); // outer, middle, inner
    }

    #[test]
    fn test_error_sources() {
        let err = make_error();
        let sources = error_sources(&err);
        assert_eq!(sources.len(), 3);
        assert_eq!(sources[0], "[config] error");
        assert_eq!(sources[1], "reading settings failed");
        assert!(sources[2].contains("file not found"));
    }

    #[test]
    fn test_inline_display() {
        let err = make_error();
        let inline = display_inline(&err);
        assert!(inline.contains(" -> "));
        assert!(inline.starts_with("[config] error"));
    }

    #[test]
    fn test_source_chain() {
        let err = make_error();
        // Level 0: AppError
        assert_eq!(err.to_string(), "[config] error");
        // Level 1: ConfigError
        let src1 = err.source().unwrap();
        assert_eq!(src1.to_string(), "reading settings failed");
        // Level 2: IoError
        let src2 = src1.source().unwrap();
        assert!(src2.to_string().contains("file not found"));
        // Level 3: None
        assert!(src2.source().is_none());
    }

    #[test]
    fn test_single_error_chain() {
        let err = IoError::FileNotFound("test.txt".into());
        let chain = display_error_chain(&err);
        assert_eq!(chain, "Error: file not found: test.txt");
        assert_eq!(error_sources(&err).len(), 1);
    }

    #[test]
    fn test_display_vs_debug() {
        let err = make_error();
        // Display: human-readable
        assert_eq!(format!("{}", err), "[config] error");
        // Debug: programmer-readable with structure
        let debug = format!("{:?}", err);
        assert!(debug.contains("AppError"));
        assert!(debug.contains("ConfigError"));
    }
}
(* 1026: Custom Display for Nested Errors *)
(* Building a source chain display for nested errors *)

type inner_error =
  | IoFailed of string
  | ParseFailed of string

type middle_error = {
  operation: string;
  inner: inner_error;
}

type outer_error = {
  module_name: string;
  cause: middle_error;
}

let string_of_inner = function
  | IoFailed msg -> Printf.sprintf "IO failed: %s" msg
  | ParseFailed msg -> Printf.sprintf "parse failed: %s" msg

let string_of_middle e =
  Printf.sprintf "%s: %s" e.operation (string_of_inner e.inner)

let string_of_outer e =
  Printf.sprintf "[%s] %s" e.module_name (string_of_middle e.cause)

(* Approach 1: Full chain display *)
let display_chain e =
  let lines = [
    Printf.sprintf "Error: %s" (string_of_outer e);
    Printf.sprintf "  Caused by: %s" (string_of_middle e.cause);
    Printf.sprintf "  Root cause: %s" (string_of_inner e.cause.inner);
  ] in
  String.concat "\n" lines

(* Approach 2: Generic error chain *)
type error_node = {
  message: string;
  source: error_node option;
}

let rec display_error_chain depth node =
  let prefix = String.make (depth * 2) ' ' in
  let label = if depth = 0 then "Error" else "Caused by" in
  let line = Printf.sprintf "%s%s: %s" prefix label node.message in
  match node.source with
  | None -> line
  | Some src -> line ^ "\n" ^ display_error_chain (depth + 1) src

let test_nested_display () =
  let err = {
    module_name = "config";
    cause = {
      operation = "reading settings";
      inner = IoFailed "file not found";
    }
  } in
  let s = string_of_outer err in
  assert (String.length s > 0);
  assert (s = "[config] reading settings: IO failed: file not found");
  Printf.printf "  Nested display: passed\n"

let test_chain_display () =
  let err = {
    message = "[config] reading settings failed";
    source = Some {
      message = "IO failed";
      source = Some {
        message = "file not found: /etc/app.conf";
        source = None;
      }
    }
  } in
  let output = display_error_chain 0 err in
  assert (String.length output > 0);
  let lines = String.split_on_char '\n' output in
  assert (List.length lines = 3);
  Printf.printf "  Chain display: passed\n"

let test_single_error () =
  let err = { message = "simple error"; source = None } in
  assert (display_error_chain 0 err = "Error: simple error");
  Printf.printf "  Single error: passed\n"

let () =
  Printf.printf "Testing error display:\n";
  test_nested_display ();
  test_chain_display ();
  test_single_error ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Custom Error Display โ€” Comparison

Core Insight

Error messages need layers: "what went wrong" (Display), "why" (source chain), and "where" (Debug). Rust's `Error` trait standardizes all three.

OCaml Approach

  • Manual `string_of_*` functions at each level
  • Nested record types with explicit `inner`/`source` fields
  • Chain display by recursive function
  • No standard trait โ€” each project defines its own convention

Rust Approach

  • `Display` trait: human-readable message for THIS error only
  • `Error::source()`: returns reference to underlying cause
  • Walk chain with `while let Some(e) = current.source()`
  • `Debug` trait: programmer-readable with full structure

Comparison Table

AspectOCamlRust
Display`string_of_*` function`impl Display`
Source chainManual field access`Error::source()` method
Chain walkingRecursive functionWhile-let loop
Standard traitNo`std::error::Error`
Debug output`[@@deriving show]` (ppx)`#[derive(Debug)]`
Inline formatManual concatenation`display_inline` via `join(" -> ")`