Designing Errors
Two questions decide an error design. Both should be answered in the skeleton, not left to the agent.
Will callers react to specific failures, or only report them?
If callers need to react to specific failures, they need an enum to match on:
#[derive(Debug)]
pub enum DecodeError {
UnknownMessage(CanId),
Truncated { need: usize, got: usize },
Db(DbcParseError), // the cause travels in the variant
}
impl std::fmt::Display for DecodeError { /* one line per variant */ }
impl std::error::Error for DecodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
DecodeError::Db(e) => Some(e), // preserved in the chain
_ => None,
}
}
}
If callers will only report the failure (log it, show it, abort the
request), an opaque error such as Box<dyn Error + Send + Sync>, with
good context attached, is a reasonable and cheap choice.
A common rule of thumb: libraries enumerate, applications aggregate. A library can’t know what its callers will match on. An application usually knows that nothing will.
Where does context attach?
An error should pick up a line of context at each boundary it crosses.
The crossing wraps the cause in a variant that adds what the boundary
knows (which profile, which file), and it exposes the cause through
source().
The final report then reads as a chain, for example “failed to load
profile: dbc parse error: truncated line 41”, rather than a leaf error
with no path back to the operation that failed.