Rust Error Source Chains
An error source chain is the stack of errors behind an error, linked by the source() method. Each error in the stack names its cause.
Display shows only the top
Printing the error gives your sentence and nothing else.
let err = load_port("/no/such/config.txt").unwrap_err();
println!("{}", err);One step down with source()
source() hands back the error yours is standing on.
if let Some(cause) = err.source() {
println!("caused by: {}", cause);
}Walk until source() says None
Keep asking each cause for its own cause. The loop is the whole technique.
let mut cause = err.source();
while let Some(c) = cause {
println!(" Caused by: {}", c);
cause = c.source();
}Example
use std::error::Error;
use std::fmt;
use std::fs;
#[derive(Debug)]
enum ConfigError {
Read(std::io::Error),
Parse(std::num::ParseIntError),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::Read(_) => write!(f, "could not read the config file"),
ConfigError::Parse(_) => write!(f, "config value is not a number"),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::Read(e) => Some(e),
ConfigError::Parse(e) => Some(e),
}
}
}
impl From<std::io::Error> for ConfigError {
fn from(e: std::io::Error) -> ConfigError { ConfigError::Read(e) }
}
impl From<std::num::ParseIntError> for ConfigError {
fn from(e: std::num::ParseIntError) -> ConfigError { ConfigError::Parse(e) }
}
fn load_port(path: &str) -> Result<u16, ConfigError> {
let text = fs::read_to_string(path)?;
Ok(text.trim().parse()?)
}
fn print_chain(e: &(dyn Error + 'static)) {
println!("Error: {}", e);
let mut cause = e.source();
while let Some(c) = cause {
println!(" Caused by: {}", c);
cause = c.source();
}
}
fn main() {
match load_port("/no/such/config.txt") {
Ok(port) => println!("port = {}", port),
Err(e) => print_chain(&e),
}
}Key points
- Display shows only the top error — your cause is not part of your message
- Ask for the next level with .source(): Some(cause), or None when the chain ends
- A loop over source() walks the whole chain, one level at a time
- The chain ends at the error whose source() returns None — usually the deepest library or OS error
- std's design rule: surface an underlying error through source() or inside your Display text.
- Never do both, or reports print it twice.
- The same walk works on any error: your enums, Box<dyn Error>, and anyhow errors all answer source()
In this example, load_port fails at the first step: the file is not there. The io error is converted by From into ConfigError::Read as it leaves through ?. print_chain prints the top error first — could not read the config file, your own sentence. Then the loop asks for source() and prints the level below. It prints: No such file or directory (os error 2). Those are the io error's own words. The io error's source() is None, so the loop stops. One generic printer now reports any error in the program, however deep its chain grows.
Note
A ready-made sources() iterator exists only on nightly Rust.
On stable Rust, the small loop in this lesson is the idiom.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
You print an error with {} and see only your own sentence. Where is the underlying io error?