Intermediate · Error handling and collections · topic 24 of 50 · 30 min
Custom error types
Explanation
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.
#[derive(Debug)]
enum OrderError {
NotFound(u32), // order id
OutOfStock { item: String },
PaymentFailed(String), // reason from the gateway
}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.
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 {}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.
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)
}Callers can now react precisely
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}"),
}Crates that remove the boilerplate
Writing Display and From by hand is repetitive. Two popular crates (libraries from crates.io) help:
| Crate | Best for | What it gives |
|---|---|---|
| thiserror | Libraries and well-defined app errors | A derive that writes Display and From for your error enum from short attributes |
| anyhow | Applications (binaries) | One easy error type, anyhow::Result<T>, that wraps any error and lets you add context messages |
// 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),
}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
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.
#[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));
}Practice (0/3 done)
Why give each failure its own enum variant instead of a String message?
Your function returns Result<u32, AppError>. Inside, "x".parse::<u32>()? fails to compile. What is missing?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Vec<T>: growable listsError handling and collections · same module
- panic! and unrecoverable errorsError handling and collections · same module
- Traits: shared behaviourModules, crates, traits and generics
- Errors as values: wrap, Is and AsGo Programming (backend and cloud) · Pointers, interfaces and errors
- Error handling: try, catch, finally and throwJavaScript Programming (language in depth) · Modules, errors, JSON and dates
- Packages, crates and modulesModules, crates, traits and generics
Questions people ask about this
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.
What are the two variants of Result?Related
Which verb in fmt.Errorf wraps an error so errors.Is can find it?Related
null.length throws which error?Related
Answer: TypeError
Reading a property of null is a TypeError.
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
- 2 more topics in “Error handling and collections” before its exam: Vec<T>: growable lists, HashMap and HashSet.
- Module 6 exam: Error handling and collections · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Modules, crates, traits and generics. 50 topics left in the course before the final exam.