We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Errors Are Values step 19 of 24
Adding context without anyhow
Rebuild anyhow’s .context() in about fifteen lines, then use it.
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>
load reads each requested key out of pairs, parses it as i32, and returns
the values in order. On the first failure it returns the fully rendered cause
chain as a string. Two failure shapes:
key `retries` is missing
key `port` is invalid: invalid digit found in string
The first has one link, the second two: your context, then std’s own
ParseIntError message, joined by ": ".
You write two things — the blanket impl of WithContext, and a render
helper that walks the chain. Everything else is given.
Why context is the thing beginners omit
invalid digit found in string is a true statement and a useless bug report.
It does not say which key, which file, or which row. The information that makes
it actionable is not available where the error is created — std has never
heard of your config file — it is available where the error is caught.
Context belongs at the call site, not in the error type’s
Display.
That is the design rule for this whole item. ConfigError::Context has a msg
field precisely because the message is decided by whoever called .context(…),
not baked into a variant.
The extension trait
impl<T, E: Error + 'static> WithContext<T> for Result<T, E> {
fn context(self, msg: &str) -> Result<T, ConfigError> {
self.map_err(|e| ConfigError::Context {
msg: msg.to_string(),
source: Box::new(e),
})
}
}
That is the trick in full. A trait with one method, blanket-implemented for
every Result whose error implements Error, so .context("…") becomes
available on results you did not write — parse(), read_to_string(),
anything. It is a map_err with a fixed shape and a nicer name, and it is
almost exactly what anyhow::Context is.
The bound the starter gets wrong
error[E0310]: the parameter type `E` may not live long enough
|
| source: Box::new(e),
| ^^^^^^^^^^^ ...so that the type `E` will meet its
| required lifetime bounds
help: consider adding an explicit lifetime bound
|
| impl<T, E: Error + 'static> WithContext<T> for Result<T, E> {
| +++++++++
Box<dyn Error + 'static> erases the type, and erasing requires knowing the
value contains no borrowed data — otherwise the box could outlive what it
points into. E: Error alone permits E = SomeError<'a>, so the compiler
stops you. The 'static here bounds the type, not a reference: “this type
holds nothing borrowed”, which every sane error type satisfies.
This is also, not coincidentally, the same bound downcast_ref needs.
The other two errors this shape produces
-
E0119 if you write a second overlapping impl — say a blanket one and a
specific one for
Result<T, ParseIntError>. Rust has no specialisation. -
E0210, the orphan rule: you may only implement a trait for a type if the
trait or the type is yours. Here
WithContextis yours, so implementing it for the foreignResultis fine.
Be careful about what that last point teaches you. In this single-file
harness every type is local, which makes the orphan rule feel like it never
bites. In a real crate, impl SomeoneElsesTrait for SomeoneElsesType is
rejected — which is exactly why the extension-trait pattern exists: you cannot
add a method to Result, so you declare your own trait and implement that.
Rendering the chain
Same walk as the previous item:
fn render(e: &dyn Error) -> String {
let mut parts = vec![e.to_string()];
let mut cur = e.source();
while let Some(s) = cur {
parts.push(s.to_string());
cur = s.source();
}
parts.join(": ")
}
Note again that ConfigError‘s own Display prints only msg — no colon, no
cause. If it printed the cause too, render would emit it twice. Display
describes one link; the chain is assembled by the caller.
Notes
-
map_err_ignoreis the lint that objects to discarding a source. Here you are doing the opposite — keeping it — which is what makes the two-link message possible. -
needless_pass_by_valuewould suggest&[(String, String)]overVec<(String, String)>for a parameter you only read. The signature here is fixed by the harness; in your own code, take the slice. -
result_large_errfires when an error type is big enough to bloat everyResultin the program. ABoxkeepsConfigErrorsmall — one more reason wrapping variants box their source. - The first failure short-circuits: once one key fails, later keys are never read.
Remember the grade is compile + tests + clippy -D warnings.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.