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

1017: Typed Error Hierarchy

Difficulty: Intermediate Category: Error Handling Concept: Building a typed error hierarchy with enum variants per subsystem Key Insight: Each subsystem defines its own error enum; the app-level enum wraps them all. `From` impls let `?` cross boundaries automatically, and pattern matching lets callers handle errors at any granularity.
// 1017: Typed Error Hierarchy
// Enum with variants for each subsystem

use std::fmt;

// Subsystem error types
#[derive(Debug, PartialEq)]
enum DbError {
    ConnectionFailed,
    QueryFailed(String),
    NotFound(String),
}

impl fmt::Display for DbError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DbError::ConnectionFailed => write!(f, "database connection failed"),
            DbError::QueryFailed(q) => write!(f, "query failed: {}", q),
            DbError::NotFound(id) => write!(f, "not found: {}", id),
        }
    }
}
impl std::error::Error for DbError {}

#[derive(Debug, PartialEq)]
enum AuthError {
    InvalidToken,
    Expired,
    Forbidden(String),
}

impl fmt::Display for AuthError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AuthError::InvalidToken => write!(f, "invalid token"),
            AuthError::Expired => write!(f, "token expired"),
            AuthError::Forbidden(r) => write!(f, "forbidden: {}", r),
        }
    }
}
impl std::error::Error for AuthError {}

#[derive(Debug, PartialEq)]
enum ApiError {
    BadRequest(String),
    RateLimit,
}

impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ApiError::BadRequest(msg) => write!(f, "bad request: {}", msg),
            ApiError::RateLimit => write!(f, "rate limited"),
        }
    }
}
impl std::error::Error for ApiError {}

// Top-level error unifies all subsystems
#[derive(Debug)]
enum AppError {
    Db(DbError),
    Auth(AuthError),
    Api(ApiError),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Db(e) => write!(f, "[DB] {}", e),
            AppError::Auth(e) => write!(f, "[Auth] {}", e),
            AppError::Api(e) => write!(f, "[API] {}", e),
        }
    }
}
impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Db(e) => Some(e),
            AppError::Auth(e) => Some(e),
            AppError::Api(e) => Some(e),
        }
    }
}

impl From<DbError> for AppError {
    fn from(e: DbError) -> Self { AppError::Db(e) }
}
impl From<AuthError> for AppError {
    fn from(e: AuthError) -> Self { AppError::Auth(e) }
}
impl From<ApiError> for AppError {
    fn from(e: ApiError) -> Self { AppError::Api(e) }
}

// Subsystem functions
fn db_find_user(id: &str) -> Result<String, DbError> {
    if id == "missing" {
        Err(DbError::NotFound(id.into()))
    } else {
        Ok(format!("user_{}", id))
    }
}

fn auth_check(token: &str) -> Result<(), AuthError> {
    if token.is_empty() {
        Err(AuthError::InvalidToken)
    } else if token == "expired" {
        Err(AuthError::Expired)
    } else {
        Ok(())
    }
}

// App layer โ€” ? auto-converts via From
fn get_user(token: &str, user_id: &str) -> Result<String, AppError> {
    auth_check(token)?;     // AuthError -> AppError
    let user = db_find_user(user_id)?; // DbError -> AppError
    Ok(user)
}


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

    #[test]
    fn test_success() {
        assert_eq!(get_user("valid", "123").unwrap(), "user_123");
    }

    #[test]
    fn test_auth_error() {
        let err = get_user("", "123").unwrap_err();
        assert!(matches!(err, AppError::Auth(AuthError::InvalidToken)));
    }

    #[test]
    fn test_expired_token() {
        let err = get_user("expired", "123").unwrap_err();
        assert!(matches!(err, AppError::Auth(AuthError::Expired)));
    }

    #[test]
    fn test_db_error() {
        let err = get_user("valid", "missing").unwrap_err();
        assert!(matches!(err, AppError::Db(DbError::NotFound(_))));
    }

    #[test]
    fn test_display_format() {
        let err = AppError::Db(DbError::QueryFailed("SELECT *".into()));
        assert_eq!(err.to_string(), "[DB] query failed: SELECT *");

        let err = AppError::Auth(AuthError::Expired);
        assert_eq!(err.to_string(), "[Auth] token expired");
    }

    #[test]
    fn test_error_source() {
        use std::error::Error;
        let err = AppError::Db(DbError::ConnectionFailed);
        let source = err.source().unwrap();
        assert_eq!(source.to_string(), "database connection failed");
    }

    #[test]
    fn test_pattern_matching_exhaustive() {
        // The compiler ensures all subsystems are handled
        fn handle(err: AppError) -> &'static str {
            match err {
                AppError::Db(_) => "database issue",
                AppError::Auth(_) => "auth issue",
                AppError::Api(_) => "api issue",
            }
        }
        assert_eq!(handle(AppError::Api(ApiError::RateLimit)), "api issue");
    }
}
(* 1017: Typed Error Hierarchy *)
(* Enum with variants for each subsystem *)

type db_error = ConnectionFailed | QueryFailed of string | NotFound of string
type auth_error = InvalidToken | Expired | Forbidden of string
type api_error = BadRequest of string | RateLimit

type app_error =
  | Db of db_error
  | Auth of auth_error
  | Api of api_error

let string_of_db_error = function
  | ConnectionFailed -> "database connection failed"
  | QueryFailed q -> Printf.sprintf "query failed: %s" q
  | NotFound id -> Printf.sprintf "not found: %s" id

let string_of_auth_error = function
  | InvalidToken -> "invalid token"
  | Expired -> "token expired"
  | Forbidden r -> Printf.sprintf "forbidden: %s" r

let string_of_api_error = function
  | BadRequest msg -> Printf.sprintf "bad request: %s" msg
  | RateLimit -> "rate limited"

let string_of_app_error = function
  | Db e -> Printf.sprintf "[DB] %s" (string_of_db_error e)
  | Auth e -> Printf.sprintf "[Auth] %s" (string_of_auth_error e)
  | Api e -> Printf.sprintf "[API] %s" (string_of_api_error e)

(* Subsystem functions return their own error types *)
let db_find_user id =
  if id = "missing" then Error (NotFound id)
  else Ok (Printf.sprintf "user_%s" id)

let auth_check token =
  if token = "" then Error InvalidToken
  else if token = "expired" then Error Expired
  else Ok "valid"

(* App layer lifts sub-errors *)
let get_user token user_id =
  match auth_check token with
  | Error e -> Error (Auth e)
  | Ok _ ->
    match db_find_user user_id with
    | Error e -> Error (Db e)
    | Ok user -> Ok user

let test_hierarchy () =
  assert (get_user "valid" "123" = Ok "user_123");
  (match get_user "" "123" with
   | Error (Auth InvalidToken) -> ()
   | _ -> assert false);
  (match get_user "valid" "missing" with
   | Error (Db (NotFound _)) -> ()
   | _ -> assert false);
  Printf.printf "  Typed hierarchy: passed\n"

let test_display () =
  let err = Db (QueryFailed "SELECT *") in
  assert (string_of_app_error err = "[DB] query failed: SELECT *");
  let err = Auth Expired in
  assert (string_of_app_error err = "[Auth] token expired");
  Printf.printf "  Display formatting: passed\n"

let () =
  Printf.printf "Testing typed error hierarchy:\n";
  test_hierarchy ();
  test_display ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Typed Error Hierarchy โ€” Comparison

Core Insight

Large applications need structured errors. Both languages use nested enums/variants, but Rust's `From` trait eliminates the manual lifting that OCaml requires.

OCaml Approach

  • Nested variant types: `type app_error = Db of db_error | Auth of auth_error`
  • Manual lifting at each boundary: `Error (Db e)` / `Error (Auth e)`
  • Individual `string_of_*` functions for display
  • No standard trait for error composition

Rust Approach

  • Same enum nesting pattern: `enum AppError { Db(DbError), Auth(AuthError) }`
  • `From` impls automate lifting via `?`
  • `Display` + `Error` traits provide standard formatting
  • `source()` method chains subsystem errors

Comparison Table

AspectOCamlRust
HierarchyNested variantsNested enums
LiftingManual `Error (Db e)`Automatic via `From` + `?`
Display`string_of_*` functions`impl Display`
ExhaustivenessYes (match)Yes (match)
Source chainManual`Error::source()`
BoilerplateMedium (lifting)Medium (`From` impls)