300: Chaining Errors with source()
Tutorial Video
Text description (accessibility)
This video demonstrates the "300: Chaining Errors with source()" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Errors in real systems are causal chains: a configuration loading failure is caused by a file read failure, which is caused by a permissions denial. Key difference from OCaml: 1. **Standard protocol**: `source()` is a standard trait method — any library that implements it participates in the chain automatically.
Tutorial
The Problem
Errors in real systems are causal chains: a configuration loading failure is caused by a file read failure, which is caused by a permissions denial. Displaying only the top-level error loses the root cause. The Error::source() method creates a linked list of errors from high-level to low-level, enabling tools and users to see the complete causal chain. This is the Rust equivalent of Java's exception chaining (getCause()) and Python's raise X from Y.
🎯 Learning Outcomes
Error::source() to expose a wrapped inner error as the causesource() method iterativelyprint_error_chain function that displays the full causal hierarchysource() returns &(dyn Error + 'static)Code Example
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}Key Differences
source() is a standard trait method — any library that implements it participates in the chain automatically.while let Some(s) = e.source() loops.source() returns a borrowed reference &(dyn Error + 'static) — the chain is borrowed, not owned, preventing double-free issues.Backtrace type (stabilized in Rust 1.73) captures stack traces at error creation, complementing the causal chain.OCaml Approach
OCaml has no standard error chaining. Exceptions have a Printexc.raise_with_backtrace for preserving stack traces, but error values in Result require explicit nesting:
type 'a with_cause = { error: 'a; cause: exn option }
(* Custom traversal required; no standard chain protocol *)
Full Source
#![allow(clippy::all)]
//! # Chaining Errors with source()
//!
//! `Error::source()` creates a linked list of causes — traverse to print full chain.
use std::error::Error;
use std::fmt;
/// File error - root cause
#[derive(Debug)]
pub struct FileError {
pub path: String,
}
impl fmt::Display for FileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "file '{}' not found", self.path)
}
}
impl Error for FileError {}
/// Config error - wraps FileError
#[derive(Debug)]
pub struct ConfigError {
pub source: FileError,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "failed to read configuration")
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
/// Startup error - wraps ConfigError
#[derive(Debug)]
pub struct StartupError {
pub source: ConfigError,
}
impl fmt::Display for StartupError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "application startup failed")
}
}
impl Error for StartupError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
/// Walk the source() chain and format each error
pub fn format_error_chain(e: &dyn Error) -> String {
let mut result = format!("Error: {}", e);
let mut cause = e.source();
let mut depth = 1;
while let Some(c) = cause {
result.push_str(&format!("\n{}Caused by: {}", " ".repeat(depth), c));
cause = c.source();
depth += 1;
}
result
}
/// Collect the full error chain into a Vec
pub fn error_chain(e: &dyn Error) -> Vec<String> {
let mut chain = vec![e.to_string()];
let mut cause = e.source();
while let Some(c) = cause {
chain.push(c.to_string());
cause = c.source();
}
chain
}
/// Get the root cause
pub fn root_cause(e: &dyn Error) -> &dyn Error {
let mut current = e;
while let Some(source) = current.source() {
current = source;
}
current
}
/// Create a test error chain
pub fn make_test_chain(path: &str) -> StartupError {
StartupError {
source: ConfigError {
source: FileError {
path: path.to_string(),
},
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_length() {
let err = make_test_chain("config.toml");
let chain = error_chain(&err);
assert_eq!(chain.len(), 3);
}
#[test]
fn test_root_cause_message() {
let err = make_test_chain("missing.toml");
let chain = error_chain(&err);
assert!(chain.last().unwrap().contains("missing.toml"));
}
#[test]
fn test_source_none_for_root() {
let e = FileError {
path: "x".to_string(),
};
assert!(e.source().is_none());
}
#[test]
fn test_format_chain() {
let err = make_test_chain("app.conf");
let formatted = format_error_chain(&err);
assert!(formatted.contains("startup failed"));
assert!(formatted.contains("configuration"));
assert!(formatted.contains("app.conf"));
}
#[test]
fn test_root_cause() {
let err = make_test_chain("test.cfg");
let root = root_cause(&err);
assert!(root.to_string().contains("test.cfg"));
}
#[test]
fn test_display_messages() {
let err = make_test_chain("data.json");
assert_eq!(format!("{}", err), "application startup failed");
assert_eq!(format!("{}", err.source), "failed to read configuration");
assert_eq!(
format!("{}", err.source.source),
"file 'data.json' not found"
);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chain_length() {
let err = make_test_chain("config.toml");
let chain = error_chain(&err);
assert_eq!(chain.len(), 3);
}
#[test]
fn test_root_cause_message() {
let err = make_test_chain("missing.toml");
let chain = error_chain(&err);
assert!(chain.last().unwrap().contains("missing.toml"));
}
#[test]
fn test_source_none_for_root() {
let e = FileError {
path: "x".to_string(),
};
assert!(e.source().is_none());
}
#[test]
fn test_format_chain() {
let err = make_test_chain("app.conf");
let formatted = format_error_chain(&err);
assert!(formatted.contains("startup failed"));
assert!(formatted.contains("configuration"));
assert!(formatted.contains("app.conf"));
}
#[test]
fn test_root_cause() {
let err = make_test_chain("test.cfg");
let root = root_cause(&err);
assert!(root.to_string().contains("test.cfg"));
}
#[test]
fn test_display_messages() {
let err = make_test_chain("data.json");
assert_eq!(format!("{}", err), "application startup failed");
assert_eq!(format!("{}", err.source), "failed to read configuration");
assert_eq!(
format!("{}", err.source.source),
"file 'data.json' not found"
);
}
}
Deep Comparison
OCaml vs Rust: Error Chaining
Pattern: Source Chain
Rust
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
OCaml
type config_error = { cause: file_error option }
(* No standard trait - manual chaining *)
Pattern: Walking the Chain
Rust
let mut cause = e.source();
while let Some(c) = cause {
println!(" Caused by: {}", c);
cause = c.source();
}
OCaml
let rec walk_chain = function
| None -> ()
| Some e ->
Printf.printf " Caused by: %s\n" (string_of_error e);
walk_chain e.cause
Pattern: Finding Root Cause
Rust
fn root_cause(e: &dyn Error) -> &dyn Error {
let mut current = e;
while let Some(source) = current.source() {
current = source;
}
current
}
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Chain mechanism | Manual cause field | Error::source() |
| Standard interface | None | std::error::Error trait |
| Root cause | Manual traversal | Same pattern works |
| Pretty printing | Custom function | Loop over source() |
| Polymorphism | Exception hierarchy | dyn Error trait object |
Exercises
AppError -> ConfigError -> IoError) and implement source() at each level to expose the next.collect_error_chain(e: &dyn Error) -> Vec<String> function that collects all error messages in the chain as a vector.error_root_cause(e: &dyn Error) -> &dyn Error function that traverses source() links until it reaches the last error with no source.