Rust Error Context Without Crates
Error context without crates is a small hand-written trait that pins a note onto an error. The note wraps the real error as its source.
Use it exactly like .context()
Same call shape as anyhow — because this is the shape anyhow copied from the pattern.
let text = fs::read_to_string(path)
.note("could not read the counter file")?;The wrapper is an ordinary error
A note string, a boxed cause, and source() joining the two.
struct WithNote {
note: String,
source: Box<dyn Error + Send + Sync>,
}
impl Error for WithNote {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&*self.source)
}
}Display prints just the note
The note is the whole message. The real error stays underneath, as the source.
impl fmt::Display for WithNote {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.note)
}
}One trait gives every Result the method
Declare the method once. Implement it for Result with any error type.
trait Note<T> {
fn note(self, text: &str) -> Result<T, WithNote>;
}map_err does the wrapping
Inside the method, map_err swaps a real failure for the note that wraps it. The note is built only when the Result really is an Err.
self.map_err(|e| WithNote {
note: text.to_string(),
source: Box::new(e),
})Example
use std::error::Error;
use std::fmt;
use std::fs;
#[derive(Debug)]
struct WithNote {
note: String,
source: Box<dyn Error + Send + Sync>,
}
impl fmt::Display for WithNote {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.note)
}
}
impl Error for WithNote {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&*self.source)
}
}
trait Note<T> {
fn note(self, text: &str) -> Result<T, WithNote>;
}
impl<T, E: Error + Send + Sync + 'static> Note<T> for Result<T, E> {
fn note(self, text: &str) -> Result<T, WithNote> {
self.map_err(|e| WithNote {
note: text.to_string(),
source: Box::new(e),
})
}
}
fn load_count(path: &str) -> Result<u32, WithNote> {
let text = fs::read_to_string(path).note("could not read the counter file")?;
let n: u32 = text.trim().parse().note("counter file did not hold a number")?;
Ok(n)
}
fn print_chain(e: &(dyn Error + 'static)) {
print!("{}", e);
let mut cause = e.source();
while let Some(c) = cause {
print!(": {}", c);
cause = c.source();
}
println!();
}
fn main() {
fs::write("counter.txt", "abc").unwrap();
match load_count("counter.txt") {
Ok(n) => println!("count = {}", n),
Err(e) => print_chain(&e),
}
match load_count("gone.txt") {
Ok(n) => println!("count = {}", n),
Err(e) => print_chain(&e),
}
}Key points
- Printing walks the note's source() chain — source() hands back the cause at each step, so a printer can list the whole chain
- anyhow's with_context is the lazy twin of this idea.
- A closure builds the note only on failure.
- That style suits notes that need values from the code.
- Only the standard library is needed.
In this example, WithNote is a plain struct error. Its Display prints the note. Its source() hands back the boxed original. The Note trait adds .note() to every Result whose error implements Error, and the impl wraps failures with map_err. load_count reads like anyhow code: each fallible step gets a note in plain words. main writes a bad counter file, then asks for a missing one. print_chain walks each chain. It shows the note first. Then it shows the real cause: counter file did not hold a number: invalid digit found in string. No crates were used anywhere.
Note
The hand-written pattern runs to about forty lines.
anyhow also adds backtraces and downcasting.
Take the crate when those are needed.
Take this pattern when a dependency cannot be added.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why is the original error boxed inside the note type?