• 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
// Type alias — this is essentially what anyhow::Result<T> is
type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
fn parse_port(s: &str) -> AnyResult<u16> {
let n: u16 = s.parse()?; // ParseIntError gets boxed automatically — no From impl needed
if n == 0 { return Err("port cannot be zero".into()); } // &str -> Box<dyn Error> via .into()
Ok(n)
}
// Adding context: wrap the box in another box with a message
fn load_config(port_str: &str) -> AnyResult<String> {
let port = parse_port(port_str)
.map_err(|e| format!("invalid port: {}", e))?; // contextual message wraps the box
Ok(format!("localhost:{}", port))
}
// main() can return Box<dyn Error> too — Rust prints it on failure
fn main() -> Result<(), Box<dyn Error>> {
let addr = load_config("8080")?;
println!("{}", addr);
Ok(())
}
The `Send + Sync` bounds matter: without them, you can't send the error across threads, which kills async code. Always use `Box<dyn Error + Send + Sync>`.
| Concept | OCaml | Rust |
|---|---|---|
| Untyped error | `exn` — exceptions are polymorphic by default | `Box<dyn Error>` — explicit type erasure |
| Any error | Raise any exception | Any type implementing `Error` can be boxed |
| Context | Wrap in new exception | Wrap with `map_err` or `.context()` extension |
| Library vs app | Same either way | Library: typed enum; App: `Box<dyn Error>` |
| Matching on variants | Pattern match exceptions | Not possible after boxing — use typed errors for libraries |
//! # anyhow-style Boxed Errors
//!
//! `Box<dyn Error + Send + Sync>` is a universal error container — the anyhow pattern.
use std::error::Error;
use std::fmt;
/// Type alias for ergonomics (what anyhow::Result is essentially)
pub type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
/// A simple context wrapper
#[derive(Debug)]
struct WithContext {
context: String,
source: Box<dyn Error + Send + Sync>,
}
impl fmt::Display for WithContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.context)
}
}
impl Error for WithContext {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.source.as_ref())
}
}
/// Extension trait for adding context (like anyhow's .context())
pub trait ResultExt<T> {
fn context(self, msg: &str) -> AnyResult<T>;
}
impl<T, E: Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
fn context(self, msg: &str) -> AnyResult<T> {
self.map_err(|e| {
Box::new(WithContext {
context: msg.to_string(),
source: Box::new(e),
}) as Box<dyn Error + Send + Sync>
})
}
}
/// Parse port number
pub fn parse_port(s: &str) -> AnyResult<u16> {
let n: u16 = s.parse()?; // ? boxes any error
if n == 0 {
return Err("port cannot be zero".into()); // .into() on &str!
}
Ok(n)
}
/// Load configuration
pub fn load_config(port_str: &str, host: &str) -> AnyResult<String> {
let port = parse_port(port_str).map_err(|e| {
Box::new(WithContext {
context: "invalid port number".to_string(),
source: e,
}) as Box<dyn Error + Send + Sync>
})?;
if host.is_empty() {
return Err("empty hostname".into());
}
Ok(format!("{}:{}", host, port))
}
/// String literal as error
pub fn require_non_empty(s: &str) -> AnyResult<&str> {
if s.is_empty() {
Err("value cannot be empty".into())
} else {
Ok(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_config_ok() {
let result = load_config("8080", "localhost");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "localhost:8080");
}
#[test]
fn test_parse_port_bad() {
assert!(parse_port("abc").is_err());
}
#[test]
fn test_empty_host() {
assert!(load_config("8080", "").is_err());
}
#[test]
fn test_port_zero() {
assert!(parse_port("0").is_err());
}
#[test]
fn test_string_literal_error() {
let result = require_non_empty("");
assert!(result.is_err());
}
#[test]
fn test_context_preserves_source() {
let pe: Result<u16, std::num::ParseIntError> = "abc".parse();
let result = pe.context("parsing port");
assert!(result.is_err());
let e = result.unwrap_err();
assert!(e.source().is_some());
}
}
(* 298. anyhow-style boxed errors - OCaml *)
(* OCaml: exceptions serve as anyhow-style dynamic errors *)
exception AnyError of string * exn option
let anyhow msg = Error (AnyError (msg, None))
let context msg = function
| Error e -> Error (AnyError (msg, Some e))
| Ok _ as r -> r
let () =
let parse s =
match int_of_string_opt s with
| Some n -> Ok n
| None -> anyhow ("failed to parse: " ^ s)
in
let result =
parse "abc"
|> context "while reading config"
in
(match result with
| Ok n -> Printf.printf "Value: %d\n" n
| Error (AnyError (msg, Some (AnyError (cause, _)))) ->
Printf.printf "Error: %s\n Caused by: %s\n" msg cause
| Error (AnyError (msg, _)) -> Printf.printf "Error: %s\n" msg
| _ -> ())