โข Option
โข Result
โข 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
โข Option
โข Result
โข 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
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.
| Concept | OCaml | Rust |
|---|---|---|
| Multiple errors | Polymorphic variant union | Enum with one variant per source |
| Conversion at `?` | Explicit `map_error` | `From` impl โ compiler inserts the call |
| Type safety | High โ variants are typed | High โ each variant wraps its specific type |
| vs. Box<dyn Error> | N/A | Enum = 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)