Error Context
Error context is high-level operational information—such as a file path, configuration key, or network endpoint—attached to a low-level error as it propagates up the call stack.
Why Low-Level Errors Need Context
A low-level function such as std::fs::read_to_string(path) or str::parse::<u16>() only knows what failed at the system or syntax level (No such file or directory or invalid digit found in string), not which file or setting caused the failure:
Without Context:
Error: No such file or directory (os error 2)
With Context:
Error: failed to read TLS certificate at "/etc/app/cert.pem"
Caused by: No such file or directory (os error 2)
Attaching Context with .map_err()
Wrapping a lower-level error inside a context-carrying struct or enum variant via .map_err(|e| ...) preserves both the high-level operation description and the underlying cause:
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct FieldParseError {
field_name: &'static str,
raw_value: String,
source: ParseIntError,
}
impl fmt::Display for FieldParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"failed to parse field '{}' from {:?}",
self.field_name, self.raw_value
)
}
}
impl Error for FieldParseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
fn parse_field(field_name: &'static str, raw: &str) -> Result<u32, FieldParseError> {
raw.trim().parse::<u32>().map_err(|err| FieldParseError {
field_name,
raw_value: raw.to_string(),
source: err,
})
}
fn main() {
if let Err(e) = parse_field("max_connections", "500x") {
println!("Error: {e}");
if let Some(cause) = e.source() {
println!(" Caused by: {cause}");
}
}
}
Error: failed to parse field 'max_connections' from "500x"
Caused by: invalid digit found in string
Reusable Context Extension Trait (with_context)
To avoid writing repetitive .map_err(...) boilerplate at every call site, you can define an extension trait on Result<T, E> that lazily evaluates a context message closure only when an Err occurs:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct ContextError {
context: String,
source: Box<dyn Error + 'static>,
}
impl fmt::Display for ContextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.context)
}
}
impl Error for ContextError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&*self.source)
}
}
pub trait ResultContextExt<T> {
fn with_context<C, F>(self, f: F) -> Result<T, ContextError>
where
C: fmt::Display,
F: FnOnce() -> C;
}
impl<T, E> ResultContextExt<T> for Result<T, E>
where
E: Error + 'static,
{
fn with_context<C, F>(self, f: F) -> Result<T, ContextError>
where
C: fmt::Display,
F: FnOnce() -> C,
{
self.map_err(|err| ContextError {
context: f().to_string(),
source: Box::new(err),
})
}
}
fn parse_timeout_ms(env_name: &str, raw: &str) -> Result<u64, ContextError> {
let seconds: u64 = raw
.parse()
.with_context(|| format!("invalid timeout in environment variable {env_name}"))?;
Ok(seconds * 1000)
}
fn main() {
match parse_timeout_ms("DB_TIMEOUT_SECS", "thirty") {
Ok(ms) => println!("Timeout: {ms} ms"),
Err(err) => {
println!("Error: {err}");
let mut cur = err.source();
while let Some(cause) = cur {
println!(" Caused by: {cause}");
cur = cause.source();
}
}
}
}
Error: invalid timeout in environment variable DB_TIMEOUT_SECS
Caused by: invalid digit found in string