298: The anyhow Pattern — Boxed Errors
Tutorial Video
Text description (accessibility)
This video demonstrates the "298: The anyhow Pattern — Boxed Errors" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Application code (as opposed to library code) often doesn't need to classify errors precisely — it just needs to propagate them to a top-level handler that logs or displays them. Key difference from OCaml: 1. **Type erasure**: `Box<dyn Error>` erases the concrete error type at runtime; the dynamic dispatch allows uniform handling of any error.
Tutorial
The Problem
Application code (as opposed to library code) often doesn't need to classify errors precisely — it just needs to propagate them to a top-level handler that logs or displays them. Defining a custom error enum for every function that calls multiple libraries is over-engineering. The anyhow pattern uses Box<dyn Error + Send + Sync> as a universal error container — any error can be boxed and propagated without defining wrapper types.
🎯 Learning Outcomes
Box<dyn Error + Send + Sync> as a type-erased error containerContext wrapper that adds descriptive messages to errorsanyhow (applications) vs thiserror (libraries)source() to display full contextCode Example
type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
fn parse_port(s: &str) -> AnyResult<u16> {
let n: u16 = s.parse()?; // auto-boxed
if n == 0 { return Err("port cannot be zero".into()); }
Ok(n)
}Key Differences
Box<dyn Error> erases the concrete error type at runtime; the dynamic dispatch allows uniform handling of any error.anyhow (and this pattern) preserves the original error as source() — the context wraps but doesn't replace.anyhow/boxed errors are appropriate for applications; libraries should use precise error types via thiserror.Send + Sync bounds enable using errors across async tasks and threads — essential for concurrent applications.OCaml Approach
OCaml uses result with string errors for simple cases, or Printexc for exceptions. The Error_monad from Tezos and Lwt's error handling provide richer composable errors, but there is no standard Box<dyn Error> equivalent:
(* Simple approach: use string as universal error *)
type 'a result_with_context = ('a, string) result
let with_context ctx = function
| Error e -> Error (ctx ^ ": " ^ e)
| Ok v -> Ok v
Full Source
#![allow(clippy::all)]
//! # 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());
}
}#[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());
}
}
Deep Comparison
OCaml vs Rust: anyhow Pattern
Pattern: Universal Error Container
Rust
type AnyResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
fn parse_port(s: &str) -> AnyResult<u16> {
let n: u16 = s.parse()?; // auto-boxed
if n == 0 { return Err("port cannot be zero".into()); }
Ok(n)
}
OCaml
(* Use exceptions for untyped errors *)
exception AppError of string
let parse_port s =
match int_of_string_opt s with
| None -> raise (AppError "invalid port")
| Some n when n = 0 -> raise (AppError "port cannot be zero")
| Some n -> n
Pattern: Adding Context
Rust
let port = parse_port(port_str).context("invalid port")?;
OCaml
try parse_port s with
| AppError msg -> raise (AppError ("invalid port: " ^ msg))
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Untyped error | exn (exceptions) | Box<dyn Error> |
| String as error | Failure "msg" | "msg".into() |
| Context | Catch and re-raise | .context() extension |
| Library vs app | Same mechanism | Library: typed; App: boxed |
| Thread safety | N/A | + Send + Sync bounds |
Exercises
context(msg) extension method on Result<T, E> that wraps any error in a WithContext struct.AnyResult<T> to unify them without any map_err.WithContext error and display each level with its message.