Skip to content

← Errors Are Values step 16 of 24

Medium Primitives

Error chains and source()

Implement chain and rendered using the supplied error types and build(depth) function. Keep their existing messages and source links.

pub fn chain(depth: usize) -> Vec<String>
pub fn rendered(depth: usize) -> String

chain returns each error's Display text, starting with the outer error and following source() until it returns None. rendered joins those strings with exactly ": ", with no leading or trailing separator.

Required output

The builder always creates a ConfigError. Its depth argument controls which causes are attached:

Depth Layers, in output order
0 or 1 ConfigError
2 ConfigError, FieldError
3 or greater ConfigError, FieldError, ParseIntError

The exact messages are:

  1. failed to load configuration
  2. key `port` is not an integer
  3. invalid digit found in string

For example, chain(3) returns these three strings in a vector. rendered(3) returns:

failed to load configuration: key `port` is not an integer: invalid digit found in string

Both rendered(0) and rendered(1) return just "failed to load configuration". Even usize::MAX produces only three layers. Follow the links rather than using depth as the number of iterations.

The test wrapper reads depth and reports both functions' outputs.

What source() provides

Each error describes its own part of the failure. Here the outer message names the failed operation, the middle message identifies the field, and the parser explains why the value was rejected. The reporting code combines them without needing to know each concrete error type.

Display does not automatically walk the chain. These supplied implementations leave their causes out of their own messages, so joining the layers does not repeat the same information.

The starter calls top.sources(), which fails with E0599 on the exercise's Rust 1.98.1 toolchain. The similarly named experimental sources() iterator belongs to dyn Error; it is not a stable trait method available on every concrete error. Use the stable source() method here. It returns one optional cause, not the entire chain, and does not include the error you called it on. Remember to record the top-level error too.

Keep the owning error alive

A source is borrowed from the error that exposes it. Binding the outer error to a variable keeps it available while you follow those borrows. This complete example walks a smaller two-layer chain:

use std::{error::Error, fmt, num::ParseIntError};

#[derive(Debug)]
struct SettingError(ParseIntError);

impl fmt::Display for SettingError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("invalid setting")
    }
}

impl Error for SettingError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Some(&self.0)
    }
}

fn example_error() -> SettingError {
    SettingError("12x".parse::<i32>().unwrap_err())
}

fn main() {
    let top = example_error();
    let mut messages = vec![top.to_string()];
    let mut current = top.source();
    while let Some(error) = current {
        messages.push(error.to_string());
        current = error.source();
    }
    assert_eq!(messages.join(": "), "invalid setting: invalid digit found in string");
}

If you instead store example_error().source() and use that borrowed source in a later statement, the temporary owner has already been dropped. Rust rejects that use with E0716. A named owner avoids this problem.

In Option<&(dyn Error + 'static)>, the outer reference is still tied to the borrow of self. The 'static bound applies to the concrete error type, not the duration of that borrow. The type may own its data or contain static references; it cannot rely on references with shorter lifetimes.

Render after collecting

Vec<String> can be joined with join(": "). This naturally inserts separators between messages and handles the one-message case without a trailing colon. The supplied chains are finite and end at None; neither function needs to return a Result for this task.

A cause chain describes related errors. A stack backtrace records call frames and is a separate tool: stable Rust supports std::backtrace::Backtrace::capture() without using the experimental Error::provide() API. No backtrace is needed for this exercise.

Your solution must compile, match the outputs and pass Clippy with warnings treated as errors.