Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.