Traits and Custom Error Types
Lending a book can fail in three ways: a bad card number, a title the library does not hold, a book already out. Returning a String for each loses information, and a caller cannot match on a sentence. The Rust answer is your own error enum, wired up with a few standard traits.
The rule
A trait is a promised behaviour; an error type is an enum that keeps those promises.
A trait lists methods a type promises to provide:
trait Describe {
fn describe(&self) -> String;
fn shout(&self) -> String { // default body
self.describe().to_uppercase()
}
}
impl Describe for Book {
fn describe(&self) -> String {
format!("{} ({} pages)", self.title, self.pages)
}
}Any type that implements Describe can be used where one is required, and it gets shout for free. Common standard traits can be derived: #[derive(Debug, Clone, PartialEq)].
A custom error is an enum, one variant per failure:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum LoanError {
BadCard(ParseIntError),
NotFound(String),
AlreadyOnLoan,
}Three traits make it a proper error:
impl fmt::Display for LoanError { // readable message
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LoanError::BadCard(e) => write!(f, "bad card number: {e}"),
LoanError::NotFound(t) => write!(f, "no book called {t}"),
LoanError::AlreadyOnLoan => write!(f, "that book is already out"),
}
}
}
impl std::error::Error for LoanError {} // needs Debug + Display
impl From<ParseIntError> for LoanError { // lets ? convert
fn from(e: ParseIntError) -> Self {
LoanError::BadCard(e)
}
}Now one signature covers every failure, and ? converts the parse error on its own:
fn lend(card: &str, title: &str) -> Result<u32, LoanError> {
let card = card.parse::<u32>()?; // ParseIntError → BadCard
if title != "Iron Tides" {
return Err(LoanError::NotFound(title.to_string()));
}
Ok(card)
}In larger projects, crates such as thiserror write this boilerplate for you, but it is the same three traits underneath.
The shortcut
Build an error type as a checklist: enum → Debug → Display → Error → From for each foreign error.
Enum
Add a variant:
CardExpired { card: u32 }.
The trap is forgetting the From impl, then wondering why ? reports that it cannot convert ParseIntError into LoanError.
Drills
Answer fast — trust the rule.
What does impl Describe for Book do?
Which trait gives LoanError its human-readable message for {}?
Why does ? turn a ParseIntError into LoanError::BadCard?
What must a type already implement before impl std::error::Error for LoanError {} compiles?
Remember
- A trait names behaviour;
impl Trait for Typeprovides it, and default methods come free. - Error checklist: enum → Debug → Display → Error → From.
- Trap: a missing
Fromimpl makes?refuse to convert the error.
