Skip to content

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.

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.

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 money
▶ Try it Yourself

Now 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 funds
▶ Try it Yourself

Now 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.

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 String error 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 match on the specific error.
  • Implement Display (impl fmt::Display for YourError) so the error type can be printed with {}.
  • In larger projects, crates like thiserror and anyhow remove most of the boilerplate.