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

297: The thiserror Pattern

Difficulty: 3 Level: Advanced Eliminate error boilerplate with derive macros โ€” understand what gets generated.

The Problem This Solves

Every production error type in Rust requires the same boilerplate: a `Display` impl, an `Error` impl, and `From` impls for each wrapped variant. For an enum with five variants, that's 30+ lines of mechanical code that adds noise without adding meaning. It's the same pattern every single time. The `thiserror` crate condenses this to a handful of annotations. `#[error("connection to '{host}' failed")]` generates the entire `Display` impl. `#[from]` generates the `From` impl. What was 30 lines becomes 5. But more importantly: the intent is visible right next to the type declaration โ€” you don't have to hunt through impl blocks to understand what each variant displays as. This example shows the full manual implementation as if you were writing what `thiserror` generates. Understanding the shape of the generated code is essential โ€” it's what you'll read in stack traces, it's what gets compiled, and it's what you need to write when you're working in a no-std environment or can't add the crate.

The Intuition

`thiserror`'s derive macro generates `Display`, `Error`, and `From` impls from annotations โ€” what was 30 lines of boilerplate becomes 5 lines of intent.

How It Works in Rust

// With thiserror (what you'd write in production):
// #[derive(thiserror::Error, Debug)]
// pub enum DbError {
//     #[error("connection to '{host}' failed")]
//     ConnectionFailed { host: String },
//     #[error("query failed: {0}")]
//     QueryFailed(String),
//     #[error(transparent)]  // delegate Display AND source() to the wrapped error
//     Io(#[from] std::io::Error),
// }

// Without thiserror โ€” the manual equivalent:
#[derive(Debug)]
pub enum DbError {
 ConnectionFailed { host: String },
 QueryFailed(String),
}

impl fmt::Display for DbError {  // what #[error("...")] generates
 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     match self {
         DbError::ConnectionFailed { host } =>
             write!(f, "connection to '{}' failed", host),
         DbError::QueryFailed(sql) =>
             write!(f, "query failed: {}", sql),
     }
 }
}

impl Error for DbError {}  // what #[derive(thiserror::Error)] generates

// What #[from] on a variant generates:
impl From<DbError> for AppError {
 fn from(e: DbError) -> Self { AppError::Db(e) }
}
The `source()` method is also generated automatically when a variant holds a `#[source]` or `#[from]` field โ€” it returns `Some(&inner_error)` for error chaining.

What This Unlocks

Key Differences

ConceptOCamlRust (manual)Rust (thiserror)
Error displayFormat string in handler`impl Display` block`#[error("...")]` annotation
Wrapping errorsManual constructor`impl From<E>``#[from]` field attribute
Source chainManual field access`fn source()` implAuto-generated from `#[from]`
BoilerplateMinimal โ€” no trait~30 lines per error type~5 lines per error type
//! 297. thiserror-style derive macros
//!
//! Manually implementing what `#[derive(thiserror::Error)]` generates.

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

// --- What thiserror generates for you ---

// #[derive(thiserror::Error, Debug)]
// pub enum DbError {
//     #[error("connection to '{host}' failed")]
//     ConnectionFailed { host: String },
//     #[error("query failed: {0}")]
//     QueryFailed(String),
// }

// Here's the manual equivalent:

#[derive(Debug)]
pub enum DbError {
    ConnectionFailed { host: String },
    QueryFailed(String),
}

impl fmt::Display for DbError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            DbError::ConnectionFailed { host } =>
                write!(f, "connection to '{}' failed", host),
            DbError::QueryFailed(sql) =>
                write!(f, "query failed: {}", sql),
        }
    }
}

impl Error for DbError {}

#[derive(Debug)]
pub enum AppError {
    Db(DbError),
    Auth(String),
    Config { key: String, reason: String },
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Db(e) => write!(f, "database error: {}", e),
            AppError::Auth(msg) => write!(f, "auth error: {}", msg),
            AppError::Config { key, reason } =>
                write!(f, "config error for '{}': {}", key, reason),
        }
    }
}

impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Db(e) => Some(e),
            _ => None,
        }
    }
}

// From impls (what #[from] generates)
impl From<DbError> for AppError {
    fn from(e: DbError) -> Self { AppError::Db(e) }
}

fn connect(host: &str) -> Result<(), DbError> {
    if host == "bad-host" {
        Err(DbError::ConnectionFailed { host: host.to_string() })
    } else {
        Ok(())
    }
}

fn run(host: &str) -> Result<(), AppError> {
    connect(host)?; // From<DbError> for AppError
    Ok(())
}

fn main() {
    let errors: Vec<AppError> = vec![
        AppError::Db(DbError::ConnectionFailed { host: "localhost".to_string() }),
        AppError::Auth("invalid token".to_string()),
        AppError::Config { key: "port".to_string(), reason: "missing".to_string() },
    ];
    for e in &errors {
        println!("{}", e);
    }

    match run("bad-host") {
        Ok(()) => println!("Connected!"),
        Err(ref e) => {
            println!("Failed: {}", e);
            if let Some(src) = e.source() {
                println!("  Source: {}", src);
            }
        }
    }
}

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

    #[test]
    fn test_db_display() {
        let e = DbError::ConnectionFailed { host: "localhost".to_string() };
        assert!(format!("{}", e).contains("localhost"));
    }

    #[test]
    fn test_from_conversion() {
        let db_err = DbError::QueryFailed("SELECT *".to_string());
        let app_err: AppError = db_err.into();
        assert!(matches!(app_err, AppError::Db(_)));
    }

    #[test]
    fn test_source_chain() {
        let app_err = AppError::Db(DbError::QueryFailed("bad".to_string()));
        assert!(app_err.source().is_some());
    }
}
(* 297. thiserror-style derive macros - OCaml *)
(* OCaml: errors are plain types, minimal boilerplate *)

type db_error = ConnectionFailed of string | QueryFailed of string
type auth_error = InvalidToken | TokenExpired of int
type app_error =
  | Db of db_error
  | Auth of auth_error
  | Config of string

let display_db = function
  | ConnectionFailed host -> Printf.sprintf "connection to '%s' failed" host
  | QueryFailed sql -> Printf.sprintf "query failed: %s" sql

let display_auth = function
  | InvalidToken -> "invalid token"
  | TokenExpired ts -> Printf.sprintf "token expired at %d" ts

let display_app = function
  | Db e -> "database error: " ^ display_db e
  | Auth e -> "auth error: " ^ display_auth e
  | Config s -> "config error: " ^ s

let () =
  let errors = [
    Db (ConnectionFailed "localhost");
    Auth TokenExpired 1234567890;
    Config "missing key 'port'";
  ] in
  List.iter (fun e -> Printf.printf "%s\n" (display_app e)) errors