Skip to content

Intermediate · Error handling and collections · topic 24 of 50 · 30 min

Custom error types

Explanation

Real programs have several kinds of failure. An error enum lists them, one variant per kind, so callers can react to each. Implement Display (for the message) and std::error::Error, and add From conversions so ? converts lower-level errors automatically. The thiserror crate writes this code for libraries; anyhow is a convenient catch-all for applications.

Why not just use String errors?

Result<T, String> is easy, but the caller only gets text. It cannot tell "file missing" (maybe create it) from "bad number on line 7" (show the line to the user) without searching inside the message, which is fragile. A custom error type gives each kind of failure its own variant, so callers can match on it.

rust
#[derive(Debug)]
enum OrderError {
    NotFound(u32),                 // order id
    OutOfStock { item: String },
    PaymentFailed(String),         // reason from the gateway
}
An error enum: each variant is one kind of failure, with the data needed to explain it.
Think of it like this: A String error is like a customer complaint written on a loose piece of paper: someone has to read it to know what to do. An error enum is like a complaint form with tick boxes ("wrong item", "late", "damaged"): each kind goes straight to the right desk.

Display: the human message

Errors should print well. Implement the Display trait so {} works. Traits are covered in module 7; for now, copy the pattern: write a fmt function that uses write! to produce the message.

rust
use std::fmt;

impl fmt::Display for OrderError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            OrderError::NotFound(id) => write!(f, "order {id} not found"),
            OrderError::OutOfStock { item } => write!(f, "{item} is out of stock"),
            OrderError::PaymentFailed(why) => write!(f, "payment failed: {why}"),
        }
    }
}

impl std::error::Error for OrderError {}
The empty impl of std::error::Error marks it as a proper error type, so it works with Box<dyn Error>.

From: letting ? convert errors

Suppose your function returns Result<_, AppError>, and inside it you call parse(), which fails with ParseIntError. The ? operator will convert ParseIntError into AppError automatically if you implement From<ParseIntError> for AppError.

rust
use std::num::ParseIntError;

#[derive(Debug)]
enum AppError {
    BadNumber(ParseIntError),
    Io(std::io::Error),
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::BadNumber(e)
    }
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

fn load_stock(path: &str) -> Result<u32, AppError> {
    let text = std::fs::read_to_string(path)?;   // io::Error -> AppError::Io
    let n = text.trim().parse::<u32>()?;         // ParseIntError -> AppError::BadNumber
    Ok(n)
}
Each ? quietly calls From::from on the error before returning it.

Callers can now react precisely

rust
match load_stock("stock.txt") {
    Ok(n) => println!("{n} in stock"),
    Err(AppError::Io(_)) => println!("stock file missing, starting from 0"),
    Err(AppError::BadNumber(e)) => println!("stock file is corrupt: {e}"),
}
Different failures, different responses.

Crates that remove the boilerplate

Writing Display and From by hand is repetitive. Two popular crates (libraries from crates.io) help:

CrateBest forWhat it gives
thiserrorLibraries and well-defined app errorsA derive that writes Display and From for your error enum from short attributes
anyhowApplications (binaries)One easy error type, anyhow::Result<T>, that wraps any error and lets you add context messages
rust
// Cargo.toml: thiserror = "2"
use thiserror::Error;

#[derive(Debug, Error)]
enum OrderError {
    #[error("order {0} not found")]
    NotFound(u32),
    #[error("could not read file")]
    Io(#[from] std::io::Error),
}
The same idea with thiserror: messages and From conversions come from attributes. Check the crate docs for the current version.

A common rule: libraries define precise error enums (often with thiserror) so their users can match on them; applications often use anyhow at the top level, where the goal is mainly to report the problem clearly.

Common mistakes

  • One giant error enum for the whole program with 40 variants. Keep error types close to the module that produces them.
  • Losing the original error. Keep it inside your variant (BadNumber(ParseIntError)) so the details are not lost.
  • Error messages with capital letters and full stops at the end. The Rust convention is short lowercase messages, because they are often joined: "could not load stock: invalid digit found in string".
  • Forgetting impl std::error::Error, so your type does not work with Box<dyn Error>.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Custom error types · 1/6

One variant per kind of failure

String errors are easy, but callers cannot tell one failure from another. A custom error enum fixes that.

Prefer a video by a teacher? Find free videos on “rust custom error types thiserror anyhow” on YouTube ↗

In real life: A booking service with clear failures

A bus-ticket booking service has three failures: the seat is taken, the payment gateway refused, and the database is down. Each is a variant of BookingError. The web layer matches on it: "seat taken" returns a 409 status with a friendly message, "payment refused" shows the bank's reason, and "database down" returns 503 and alerts the on-call engineer.

rust
#[derive(Debug)]
enum BookingError {
    SeatTaken(u8),
    PaymentRefused(String),
    DatabaseDown,
}

fn http_status(e: &BookingError) -> u16 {
    match e {
        BookingError::SeatTaken(_) => 409,
        BookingError::PaymentRefused(_) => 402,
        BookingError::DatabaseDown => 503,
    }
}

fn main() {
    let e = BookingError::SeatTaken(14);
    println!("{:?} -> {}", e, http_status(&e));
}
Prints: SeatTaken(14) -> 409

Practice (0/3 done)

Task 1 · Question

Why give each failure its own enum variant instead of a String message?

Task 2 · Question

Your function returns Result<u32, AppError>. Inside, "x".parse::<u32>()? fails to compile. What is missing?

Task 3 · Practice

Create enum LoginError { NoSuchUser(String), WrongPassword, Locked { minutes: u32 } } with Display and std::error::Error. Write fn login(user: &str, pass: &str) -> Result<(), LoginError> with a few fake rules, and in main print the message for each failure.

Quick quiz

Question 1

1.Which trait gives an error a human-readable message for {}?

Question 2

2.What does ? use to convert one error type into another?

Question 3

3.Which crate is a common choice for application-level errors with context?

Question 4

4.What is the Rust style for error messages?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which trait gives an error a human-readable message for {}?

Answer: Display

Display controls how {} prints a value.

What does ? use to convert one error type into another?

Answer: The From trait

? calls From::from on the error.

Which crate is a common choice for application-level errors with context?

Answer: anyhow

anyhow wraps any error and adds context messages.

What is the Rust style for error messages?

Answer: Short and lowercase, without a full stop

Lowercase messages read well when errors are chained together.

Can you implement std's Display for std's Vec<u8> in your crate?Related

Answer: No, because of the orphan rule

Neither the trait nor the type is yours. Wrap Vec in a newtype instead.

From Traits: shared behaviour

What are the two variants of Result?Related

Answer: Ok and Err

Result<T, E> is either Ok(T) or Err(E).

From Result and the ? operator

Which verb in fmt.Errorf wraps an error so errors.Is can find it?Related

Answer: %w

%w wraps; %v and %s only copy the text.

From Errors as values: wrap, Is and As

null.length throws which error?Related

Answer: TypeError

Reading a property of null is a TypeError.

From Error handling: try, catch, finally and throw

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Error handling and collections” before its exam: Vec<T>: growable lists, HashMap and HashSet.
  2. Module 6 exam: Error handling and collections · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Modules, crates, traits and generics. 50 topics left in the course before the final exam.

← Previous: Result and the ? operator