Skip to content

← Errors Are Values step 16 of 24

Medium Primitives

Error chains and source()

Walk an error’s cause chain and render it.

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

build(depth) is given. It produces a ConfigError that is one, two or three layers deep:

  • layer 1 — ConfigError, Display failed to load configuration
  • layer 2 — FieldError, Display key `port` is not an integer
  • layer 3 — a real std::num::ParseIntError from "12x".parse::<i32>(), Display invalid digit found in string

chain returns every layer’s Display string, top-down. rendered joins them with ": ". So chain(3) is

["failed to load configuration",
 "key `port` is not an integer",
 "invalid digit found in string"]

and rendered(3) is

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

rendered(1) is just "failed to load configuration"no trailing separator.

Why this is the payoff of the last two items

Look at that rendered line. The first clause tells an operator what failed, the second where, the third why. No single error type could have produced it: the third layer was written by the standard library years ago and knows nothing about config files, and the first knows nothing about integers. The chain is what lets each layer say only its own part — which is exactly what the Display convention from item 6.14 was preparing for.

This function is also, precisely, what anyhow‘s {:#} formatting gives you for free. Writing it once by hand is how “anyhow is convenient” becomes “anyhow saves me these nine lines”.

The starter’s E0599 is the one you will actually hit

error[E0599]: no method named `sources` found for struct `ConfigError`
   |
70 |     top.sources().map(|e| e.to_string()).collect()
   |         ^^^^^^^
help: there is a method `source` with a similar name

Error::sources() — the iterator that would make this a one-liner — is still unstable on Rust 1.95 (error_iter, tracking issue #58520). It appears in blog posts and in older documentation, so learners try it, and the error they get is a plain “no such method” rather than “this is nightly-only”. Note that rustc’s suggestion is useful but not a fix: source() returns a single Option, not an iterator.

So you write the loop:

let mut cur = e.source();
while let Some(s) = cur {
    // ...record s.to_string()...
    cur = s.source();
}

source() returns Option<&(dyn Error + 'static)>. The loop stops when a layer has no source. Do not stop it with a counter — case depth: 9 builds the same three layers, and a loop that trusts depth will run off the end or produce the wrong answer.

The borrow that catches people

E0716, “temporary value dropped while borrowed”:

let mut cur = build(depth).source();   // E0716

build(depth) creates a temporary ConfigError; .source() borrows from it; the temporary is dropped at the end of the statement; the borrow outlives it. Bind the error to a variable first, and everything the chain hands you lives as long as that variable does:

let top = build(depth);
let mut cur = top.source();

Building the string

chain(depth).join(": ") is the clean way, and it is what the reference solution does. Two notes for when you write this in real code:

  • msg.push_str(&format!("{s}: ")) allocates a whole String just to copy it into another one. Clippy has a lint for it (format_push_string, pedantic). The fix is write!:

    use std::fmt::Write as _;
    let _ = write!(out, "{s}: ");   // writing to a String cannot fail

    The use … as _ imports the trait for its methods without bringing the name Write into scope, which matters because std::io::Write exists too and they collide.

  • single_char_add_str is the sibling lint: s.push_str(":") should be s.push(':').

Notes

  • Backtrace capture lives behind Error::provide(), which is still nightly. In stable code you either store a Backtrace in your error type explicitly or you use a crate that does.
  • The Option<&(dyn Error + 'static)> return type reads oddly the first time. 'static bounds the erased type, not the borrow: it says the concrete error behind the dyn contains no borrowed data, which is what makes downcast_ref possible later.
  • clippy::missing_errors_doc would want a # Errors section on every public function returning Result. Neither function here returns one — chain-walking is infallible.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…