Rust Custom Error Types
On the Result page, every error was just a String. That’s fine for small examples, but it breaks down once a function can fail in more than one distinct way — a String can’t be matched on, so callers can’t tell which kind of failure happened without parsing the text itself.
The problem with String errors
Section titled “The problem with String errors”fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("insufficient funds")); } if amount < 0.0 { return Err(String::from("amount cannot be negative")); } Ok(balance - amount)}If a caller wants to react differently to “insufficient funds” versus “negative amount,” their only option is comparing the error string itself — brittle, and it breaks the moment you reword a message.
Defining an error as an enum
Section titled “Defining an error as an enum”An enum fixes this — each kind of failure becomes its own variant, which callers can match on directly:
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("New balance: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("Not enough money"), Err(WithdrawError::NegativeAmount) => println!("Can't withdraw a negative amount"), }}Output:
Not enough moneyNow the compiler enforces that every kind of failure is handled — same guarantee match has always given you with enums, just applied to errors.
Implementing Display for a proper error message
Section titled “Implementing Display for a proper error message”Try printing WithdrawError with {} right now, and it won’t compile — Rust doesn’t know how to turn it into text. Implementing the Display trait (which you met conceptually on the Traits page) fixes that:
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "insufficient funds"), WithdrawError::NegativeAmount => write!(f, "amount cannot be negative"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Error: {error}");}Output:
Error: insufficient fundsNow WithdrawError behaves like any other error you’d want to print to a user or a log — while still being fully matchable for code that needs to react to a specific case.
When a crate can help
Section titled “When a crate can help”Writing Display and the boilerplate around a custom error type by hand gets repetitive across a real project. The thiserror crate generates most of it for you with a #[derive], and anyhow is popular for application code that just needs to propagate errors without defining a type for each one. Worth knowing they exist; not something you need for learning the fundamentals.
- A
Stringerror works for small examples, but can’t be matched on distinctly by callers. - Define an enum with one variant per kind of failure so callers can
matchon the specific error. - Implement
Display(impl fmt::Display for YourError) so the error type can be printed with{}. - In larger projects, crates like
thiserrorandanyhowremove most of the boilerplate.