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

311: Handling Multiple Error Types

Difficulty: 3 Level: Advanced Unify errors from different sources so `?` works across an entire module.

The Problem This Solves

A real application function might call the filesystem, a database, a parser, and an HTTP client โ€” each returning its own error type. Without a unification strategy, you can't use `?` across all of them in a single function. You're stuck with nested `match` expressions or a proliferation of `map_err` calls at every site. The standard solution is a unified error enum with one variant per source, and `impl From<SourceError> for AppError` for each variant. Once those `From` impls exist, `?` handles all conversions automatically. Your function body reads cleanly โ€” just the happy path logic โ€” and the error handling is centralized in the enum definition. This pattern scales. You start with three variants. When you add a new dependency, you add one variant and one `From` impl. The call sites don't change. The function signatures stay readable. This is why virtually every non-trivial Rust application uses this pattern.

The Intuition

One unified error enum + one `From` impl per source error type = `?` works everywhere in your module.

How It Works in Rust

use std::num::ParseIntError;

// Step 1: One enum to rule them all
#[derive(Debug)]
enum AppError {
 Io(IoError),
 Db(DbError),
 Parse(ParseIntError),
}

impl fmt::Display for AppError { /* match on variants */ }

// Step 2: From impl for each source โ€” this is what enables ?
impl From<IoError> for AppError {
 fn from(e: IoError) -> Self { AppError::Io(e) }
}
impl From<DbError> for AppError {
 fn from(e: DbError) -> Self { AppError::Db(e) }
}
impl From<ParseIntError> for AppError {
 fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

// Step 3: Use ? freely โ€” no map_err anywhere
fn pipeline(path: &str) -> Result<Vec<i32>, AppError> {
 let content = read_file(path)?;    // IoError -> AppError::Io via From
 let n: i32 = content.parse()?;    // ParseIntError -> AppError::Parse via From
 let rows = query_db(n)?;           // DbError -> AppError::Db via From
 Ok(rows)
}

// Step 4: Match on specific variants at the boundary
match pipeline("data.txt") {
 Ok(rows) => process(rows),
 Err(AppError::Io(e)) => eprintln!("File error: {}", e),
 Err(AppError::Db(e)) => eprintln!("DB error: {}", e),
 Err(AppError::Parse(e)) => eprintln!("Bad data: {}", e),
}
The tradeoff vs `Box<dyn Error>`: the enum approach preserves variant information (you can match on it), while `Box<dyn Error>` erases it. Use the enum for libraries and anywhere callers need to distinguish error types; use the box for application-level propagation where you just log and exit.

What This Unlocks

Key Differences

ConceptOCamlRust
Multiple errorsPolymorphic variant unionEnum with one variant per source
Conversion at `?`Explicit `map_error``From` impl โ€” compiler inserts the call
Type safetyHigh โ€” variants are typedHigh โ€” each variant wraps its specific type
vs. Box<dyn Error>N/AEnum = matchable; Box = type-erased; use enum for libs
//! 311. Handling multiple error types
//!
//! Unify multiple error types with an enum + `impl From` for each variant.

use std::fmt;
use std::num::ParseIntError;

// ---- Source error types ----
#[derive(Debug)] struct IoError(String);
#[derive(Debug)] struct DbError(String);

impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IO: {}", self.0) }
}
impl fmt::Display for DbError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "DB: {}", self.0) }
}
impl std::error::Error for IoError {}
impl std::error::Error for DbError {}

// ---- Unified error enum ----
#[derive(Debug)]
enum AppError {
    Io(IoError),
    Db(DbError),
    Parse(ParseIntError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Db(e) => write!(f, "DB error: {}", e),
            AppError::Parse(e) => write!(f, "parse error: {}", e),
        }
    }
}

// From impls enable ? operator
impl From<IoError> for AppError {
    fn from(e: IoError) -> Self { AppError::Io(e) }
}
impl From<DbError> for AppError {
    fn from(e: DbError) -> Self { AppError::Db(e) }
}
impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}

// ---- Functions returning different error types ----
fn read_file(path: &str) -> Result<String, IoError> {
    if path == "missing" { Err(IoError(format!("{}: not found", path))) }
    else { Ok("42".to_string()) }
}

fn query_db(n: i32) -> Result<Vec<i32>, DbError> {
    if n < 0 { Err(DbError("negative input not allowed".to_string())) }
    else { Ok(vec![n, n*2, n*3]) }
}

// ---- Pipeline using ? with automatic conversion ----
fn pipeline(path: &str) -> Result<Vec<i32>, AppError> {
    let content = read_file(path)?;  // IoError -> AppError via From
    let n: i32 = content.trim().parse()?;  // ParseIntError -> AppError via From
    let rows = query_db(n)?;  // DbError -> AppError via From
    Ok(rows)
}

fn main() {
    println!("{:?}", pipeline("data.txt")); // Ok
    println!("{:?}", pipeline("missing"));  // Err(Io(...))
}

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

    #[test]
    fn test_pipeline_ok() {
        assert!(pipeline("data.txt").is_ok());
    }

    #[test]
    fn test_pipeline_io_err() {
        assert!(matches!(pipeline("missing"), Err(AppError::Io(_))));
    }

    #[test]
    fn test_from_io_error() {
        let e: AppError = IoError("test".to_string()).into();
        assert!(matches!(e, AppError::Io(_)));
    }
}
(* 311. Handling multiple error types - OCaml *)

type parse_err = ParseFail of string
type io_err = IoFail of string
type db_err = DbFail of string

type app_err =
  | ParseError of parse_err
  | IoError of io_err
  | DbError of db_err

let parse s = match int_of_string_opt s with
  | Some n -> Ok n | None -> Error (ParseError (ParseFail s))

let read_file f =
  if f = "missing" then Error (IoError (IoFail f)) else Ok "42"

let db_query q =
  if q = "bad" then Error (DbError (DbFail q)) else Ok [1; 2; 3]

let pipeline input =
  let ( let* ) = Result.bind in
  let* content = read_file input in
  let* n = parse content in
  let* rows = db_query (if n > 0 then "good" else "bad") in
  Ok (List.length rows + n)

let () =
  (match pipeline "data.txt" with
  | Ok n -> Printf.printf "Result: %d\n" n
  | Error (ParseError (ParseFail s)) -> Printf.printf "Parse error: %s\n" s
  | Error (IoError (IoFail f)) -> Printf.printf "IO error: %s\n" f
  | Error (DbError (DbFail q)) -> Printf.printf "DB error: %s\n" q)