295: Implementing std::error::Error
Tutorial Video
Text description (accessibility)
This video demonstrates the "295: Implementing std::error::Error" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The `std::error::Error` trait is the common interface for all Rust errors, enabling error chaining, dynamic dispatch, and interoperability between libraries. Key difference from OCaml: 1. **Standard interface**: Rust's `std::error::Error` is the universal error contract; OCaml has no equivalent standard trait.
Tutorial
The Problem
The std::error::Error trait is the common interface for all Rust errors, enabling error chaining, dynamic dispatch, and interoperability between libraries. Implementing it properly — with Display for user messages, Debug for developer output, and source() for causal chains — is the foundation of production-quality error handling. This mirrors the interface that anyhow, thiserror, and the broader ecosystem expect.
🎯 Learning Outcomes
std::error::Error trait: Display, Debug, and optionally source()source() to create linked error chains that expose root causesBox<dyn Error + Send + Sync> pattern for type-erased error storageSend + Sync bounds as requirements for using errors across thread boundariesCode Example
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct ParseError { input: String, reason: String }
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse '{}': {}", self.input, self.reason)
}
}
impl Error for ParseError {} // source() defaults to NoneKey Differences
std::error::Error is the universal error contract; OCaml has no equivalent standard trait.source() creates a traversable linked list of causes; OCaml requires manual nested error structures.Box<dyn Error + Send + Sync> enables sending errors across thread boundaries; OCaml's GC handles this transparently.? operator, Box<dyn Error>, anyhow, and thiserror all depend on std::error::Error as the common interface.OCaml Approach
OCaml does not have a standard error interface — errors are plain values. The idiomatic approach in modern OCaml uses Result.t with a custom error type and provides to_string for display. Libraries like Fmt provide pp_error conventions:
type error = { field: string; cause: string }
let string_of_error { field; cause } =
Printf.sprintf "field '%s' invalid: %s" field cause
OCaml lacks a standard "error chaining" mechanism — nested error types or exception causes must be manually threaded.
Full Source
#![allow(clippy::all)]
//! # Implementing std::error::Error
//!
//! `std::error::Error` requires Display + Debug and optionally provides `source()`.
use std::error::Error;
use std::fmt;
/// Low-level parse error
#[derive(Debug)]
pub struct ParseError {
pub input: String,
pub reason: String,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse '{}': {}", self.input, self.reason)
}
}
impl Error for ParseError {} // source() defaults to None
/// Higher-level validation error that wraps a cause
#[derive(Debug)]
pub struct ValidationError {
pub field: String,
pub source: Box<dyn Error + Send + Sync>,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "validation failed for field '{}'", self.field)
}
}
impl Error for ValidationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.source.as_ref())
}
}
/// Parse a string as an age (u8)
pub fn parse_age(s: &str) -> Result<u8, ParseError> {
s.parse::<u8>().map_err(|e| ParseError {
input: s.to_string(),
reason: e.to_string(),
})
}
/// Validate user age with context
pub fn validate_user_age(s: &str) -> Result<u8, ValidationError> {
parse_age(s).map_err(|e| ValidationError {
field: "age".to_string(),
source: Box::new(e),
})
}
/// Print full error chain
pub fn print_error_chain(e: &dyn Error) -> String {
let mut result = format!("Error: {}", e);
let mut cause = e.source();
while let Some(c) = cause {
result.push_str(&format!("\n Caused by: {}", c));
cause = c.source();
}
result
}
/// Collect heterogeneous errors
pub fn collect_errors() -> Vec<Box<dyn Error>> {
vec![Box::new(ParseError {
input: "x".to_string(),
reason: "not a number".to_string(),
})]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error_display() {
let e = ParseError {
input: "abc".to_string(),
reason: "invalid".to_string(),
};
let msg = format!("{}", e);
assert!(msg.contains("abc"));
}
#[test]
fn test_parse_error_is_error_trait() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "x".to_string(),
reason: "bad".to_string(),
});
assert!(e.source().is_none());
}
#[test]
fn test_validation_error_source() {
let result = validate_user_age("abc");
assert!(result.is_err());
let e = result.unwrap_err();
assert!(e.source().is_some());
}
#[test]
fn test_valid_age() {
assert_eq!(validate_user_age("25").unwrap(), 25);
}
#[test]
fn test_error_chain_string() {
let result = validate_user_age("bad");
if let Err(e) = result {
let chain = print_error_chain(&e);
assert!(chain.contains("validation failed"));
assert!(chain.contains("Caused by"));
}
}
#[test]
fn test_collect_heterogeneous() {
let errors = collect_errors();
assert_eq!(errors.len(), 1);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error_display() {
let e = ParseError {
input: "abc".to_string(),
reason: "invalid".to_string(),
};
let msg = format!("{}", e);
assert!(msg.contains("abc"));
}
#[test]
fn test_parse_error_is_error_trait() {
let e: Box<dyn Error> = Box::new(ParseError {
input: "x".to_string(),
reason: "bad".to_string(),
});
assert!(e.source().is_none());
}
#[test]
fn test_validation_error_source() {
let result = validate_user_age("abc");
assert!(result.is_err());
let e = result.unwrap_err();
assert!(e.source().is_some());
}
#[test]
fn test_valid_age() {
assert_eq!(validate_user_age("25").unwrap(), 25);
}
#[test]
fn test_error_chain_string() {
let result = validate_user_age("bad");
if let Err(e) = result {
let chain = print_error_chain(&e);
assert!(chain.contains("validation failed"));
assert!(chain.contains("Caused by"));
}
}
#[test]
fn test_collect_heterogeneous() {
let errors = collect_errors();
assert_eq!(errors.len(), 1);
}
}
Deep Comparison
OCaml vs Rust: std::error::Error Trait
Pattern 1: Basic Error Implementation
Rust
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct ParseError { input: String, reason: String }
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to parse '{}': {}", self.input, self.reason)
}
}
impl Error for ParseError {} // source() defaults to None
Pattern 2: Error with Source Chain
OCaml
exception ChainedError of string * exn
let with_context msg result =
match result with
| Ok _ as r -> r
| Error e -> Error (ChainedError (msg, e))
Rust
#[derive(Debug)]
struct ValidationError {
field: String,
source: Box<dyn Error + Send + Sync>,
}
impl Error for ValidationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.source.as_ref())
}
}
Pattern 3: Dynamic Error Collection
Rust
let errors: Vec<Box<dyn Error>> = vec![
Box::new(ParseError { ... }),
Box::new(IoError { ... }),
];
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Error trait | No standard | std::error::Error |
| Requirements | None | Display + Debug |
| Error chaining | Manual cause field | source() method |
| Dynamic dispatch | Exceptions are polymorphic | Box<dyn Error> |
| Walk chain | Manual traversal | Loop over .source() |
Exercises
std::error::Error for a three-level error chain: IoError wrapping ParseError wrapping ValidationError, and traverse the chain using source().print_error_chain(e: &dyn Error) that iterates through source() links and prints each cause on a new line.std::io::Error using Box<dyn Error + Send + Sync> and test that source() exposes the original IO error.