Skip to content

← Errors Are Values step 19 of 24

Hard Framework

Adding context without anyhow

Repair the supplied WithContext implementation, then add a render helper that includes each error's source. Update load to use render on failure. Preserve ConfigError, its Display/source implementations and the public API:

pub trait WithContext<T> {
    fn context(self, msg: &str) -> Result<T, ConfigError>;
}

pub fn load(pairs: Vec<(String, String)>, keys: Vec<String>) -> Result<Vec<i32>, String>

Lookup and parsing rules

Process keys in request order, including repeated keys. For each request:

  1. Find the first pair whose key matches exactly. Do not trim, lowercase or normalize keys. Later pairs with the same key do not replace the first.
  2. Trim Unicode whitespace from both ends of the selected value and parse it as an i32. Decimal ASCII digits may have a leading plus or minus and leading zeros. Internal whitespace, underscores, missing digits and values outside the i32 range fail.
  3. Append a successful value, or stop at the first failure and return Err.

Empty requests return Ok with an empty vector, even if pairs contains invalid values. Unrequested pairs are ignored. Empty and Unicode keys are valid; messages include keys exactly, including any punctuation or whitespace.

For pairs [("a", " -12 "), ("a", "99")] and requests ["a", "a"], return Ok([-12, -12]). For a missing key, return its one-link message:

key `retries` is missing

For a present key whose value fails parsing, return context followed by the standard parser's message, separated by a colon and space:

key `port` is invalid: invalid digit found in string

An empty trimmed value instead has the cause cannot parse integer from empty string. Obtain causes from the actual error's Display implementation; do not hardcode one parse message for every failure. Standard-library diagnostic wording can depend on the toolchain.

The wrapper reports ok, values and error. Success has an empty error string; failure has an empty values array, even if earlier requests succeeded.

Add context while retaining the error

WithContext is a local extension trait implemented for Result<T, E>. Its context method should leave Ok(T) unchanged. For Err(E), create ConfigError::Context with an owned copy of msg and the original error boxed as source. map_err expresses that transformation without requiring T or E to implement Clone.

The call site knows which key failed, while ParseIntError knows why parsing failed. Keeping both makes the report useful. This is a small standard-library context wrapper, not a replacement for every feature of an error-reporting crate.

Implementing a local trait for the standard Result type is allowed. You are adding a trait method, not an inherent method to Result. The standard types remain foreign even when the exercise is compiled as one source file.

Why the error type needs a 'static bound

ConfigError stores Box<dyn Error + 'static>. To place E into that field, the implementation needs E: Error + 'static. The starter omits the latter bound and fails with E0310.

E: 'static means E cannot depend on references with shorter lifetimes. It does not mean the value lives forever, and it does not forbid references to static data. An owned String or an error holding a string literal can satisfy the bound and still be dropped normally. Boxing or type erasure by itself does not require 'static; this field's declared contract does.

This complete example shows a valid error holding a static reference:

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct Note(&'static str);

impl fmt::Display for Note {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.0)
    }
}
impl Error for Note {}

fn main() {
    let error: Box<dyn Error + 'static> = Box::new(Note("missing setting"));
    assert_eq!(error.to_string(), "missing setting");
    drop(error);
}

Render each link once

Start with the top error's to_string(). Follow Error::source repeatedly until it returns None, collecting each source's to_string() in order. Join those messages with ": ". A source-less error produces just its own message. The supplied error chains terminate; no cycle-handling policy is needed here.

ConfigError::Context's Display prints only msg, and source exposes the boxed cause. Keep that separation: adding the cause to Display while also walking source would duplicate it. For nested contexts, render every link rather than stopping after the first source. See the Error source documentation.

Fixing the bound alone leaves the starter's second defect: load currently returns only e.to_string(), losing the lower-level message in its output. Replace that reporting step with your chain renderer.

Passing requires the specified results, successful compilation and Clippy with warnings treated as errors.