Step 5 of 7
Source chains: errors with causes
When an error wraps another one, there are two messages: the high-level one ("could not read the port setting") and the low-level cause ("invalid digit found in string"). Users need the first, and whoever fixes the problem often needs both. The Error trait has a method for this, source(), which returns the error that caused this one, if any.
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum ConfigError {
MissingKey(String),
BadNumber { key: String, source: ParseIntError },
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ConfigError::MissingKey(key) => write!(f, "missing key {key}"),
ConfigError::BadNumber { key, .. } => write!(f, "invalid value for {key}"),
}
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ConfigError::MissingKey(_) => None,
ConfigError::BadNumber { source, .. } => Some(source),
}
}
}
fn read_port(text: &str) -> Result<u16, ConfigError> {
let value = text
.strip_prefix("port=")
.ok_or_else(|| ConfigError::MissingKey(String::from("port")))?;
value.parse().map_err(|e| ConfigError::BadNumber { key: String::from("port"), source: e })
}
fn report(err: &dyn Error) {
println!("error: {err}");
let mut cause = err.source();
while let Some(c) = cause {
println!(" caused by: {c}");
cause = c.source();
}
}
fn main() {
if let Err(e) = read_port("port=80a") {
report(&e);
}
if let Err(e) = read_port("host=example.com") {
report(&e);
}
}
error: invalid value for port
caused by: invalid digit found in string
error: missing key port
Piece by piece:
fn source(&self) -> Option<&(dyn Error + 'static)>overrides the defaultsource(which returnsNone). Copy the signature as it is:&(dyn Error + 'static)means "a reference to some error"; the'staticis a lifetime saying the error doesn't borrow short-lived data (module 24).Some(source)hands out a reference to the wrappedParseIntError. Rust turns&ParseIntErrorinto&dyn Errorautomatically.reporttakes&dyn Error, so it works with any error. It prints the error, then followssource()link by link withwhile letuntil there's no further cause. This list is the error chain.- The variant names its field
source, which is a common convention (and what thiserror, step 7, looks for).
Display or source, not both
If BadNumber displayed as invalid value for port: invalid digit found in string and returned the parse error from source(), every report would print the cause twice. The guideline: an error either includes the inner message in its Display, or returns it from source(). When you wrap an error and keep it, prefer source(). Tools like anyhow print the whole chain for you, joined by : .
Common mistakes
- Forgetting
source()when wrapping. The default returnsNone, so the chain stops at your error and the real cause is invisible in reports. - Writing the signature differently.
fn source(&self) -> Option<&dyn Error>without+ 'staticdoesn't match the trait and won't compile. Copy it exactly. - Calling
.source()without the trait in scope. It's a method of theErrortrait, so you needuse std::error::Error;, like you needuse std::fmt;for Display.
Your task
The starter's LoadError has Display and an empty-ish Error implementation. Write its source method: MissingKey has no cause, and BadPort returns its source field.
Challenge 2
Write fn error_chain(err: &dyn Error) -> String that joins the messages of an error and all its sources with ": ", outermost first. For a BadPort error that's port is not a valid number: invalid digit found in string. This is exactly what anyhow prints with {:#}.
The starter has two complete error types to test with: LoadError and StartupError (which wraps a LoadError).
Challenge 3
Build a two-layer error yourself. The starter has LoadError and fn load_port(text: &str) -> Result<u16, LoadError>. Write:
AppError(deriveDebug) with one variant,Config(LoadError)- Display for
AppError:could not start the server ErrorforAppError, withsource()returning the innerLoadErrorFrom<LoadError> for AppErrorfn start(port_text: &str) -> Result<u16, AppError>that callsload_port(port_text)?and returns the port
The tests print the whole chain with the starter's error_chain.
Previous: From: teaching ? to convert errors Next: panic or Result? The guidelines