RustArena

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:

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

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:

The tests print the whole chain with the starter's error_chain.

Previous: From: teaching ? to convert errors Next: panic or Result? The guidelines