Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Types and Error Handling  ›  Capstone Project

Traits and Custom Error Types

Custom Error Types and Traits 18 minA trait names behaviour that types can implement, and a custom error type is an enum that implements Display, Error and From so every failure in a program has one clear type.

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.

Adding a new failure: card expiredStep 1 of 4
  1. 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.

quick drill

What does impl Describe for Book do?

quick drill

Which trait gives LoanError its human-readable message for {}?

quick drill

Why does ? turn a ParseIntError into LoanError::BadCard?

quick drill

What must a type already implement before impl std::error::Error for LoanError {} compiles?

Remember

  • A trait names behaviour; impl Trait for Type provides it, and default methods come free.
  • Error checklist: enum → Debug → Display → Error → From.
  • Trap: a missing From impl makes ? refuse to convert the error.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainOne error type for the library

The lending function can fail because the card number is not a number, the book is not found, or the book is already on loan. Design a LoanError type and explain which traits it implements and why ? on a ParseIntError then works inside a function returning Result<(), LoanError>.