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 fixes that. Traits get their own pages soon (Traits and Common Traits). For now, you can copy this pattern as it is:

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, but not something you need for learning the fundamentals. When you’re ready, the anyhow and thiserror page shows both in action, and when to pick each one.

Define an enum LoginError with two variants, EmptyUsername and PasswordTooShort, and implement Display for it. Then write fn check_login(user: &str, password: &str) -> Result<String, LoginError> that returns Ok with a welcome message, or the right error. A password must be at least 8 characters. Hint: .is_empty() and .len() on a &str give you what you need to check.

Show solution
use std::fmt;
enum LoginError {
EmptyUsername,
PasswordTooShort,
}
impl fmt::Display for LoginError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LoginError::EmptyUsername => write!(f, "username cannot be empty"),
LoginError::PasswordTooShort => write!(f, "password must be at least 8 characters"),
}
}
}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> {
if user.is_empty() {
return Err(LoginError::EmptyUsername);
}
if password.len() < 8 {
return Err(LoginError::PasswordTooShort);
}
Ok(format!("Welcome, {user}!"))
}
fn main() {
let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts {
match check_login(user, password) {
Ok(message) => println!("{message}"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

Welcome, ana!
Error: username cannot be empty
Error: password must be at least 8 characters
▶ Try it Yourself

Write fn parse_percent(input: &str) -> Result<u32, PercentError>, where PercentError has two variants that hold data: NotANumber(String) (the bad text) and TooLarge(u32) (the number that was over 100). Make the Display messages include those details. Hint: enum variants can hold values, as on the Enums page. In fmt, match with PercentError::TooLarge(n) => ... to get the value out.

Show solution
use std::fmt;
enum PercentError {
NotANumber(String),
TooLarge(u32),
}
impl fmt::Display for PercentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PercentError::NotANumber(text) => write!(f, "'{text}' is not a number"),
PercentError::TooLarge(n) => write!(f, "{n} is more than 100"),
}
}
}
fn parse_percent(input: &str) -> Result<u32, PercentError> {
match input.parse::<u32>() {
Ok(n) => {
if n > 100 {
Err(PercentError::TooLarge(n))
} else {
Ok(n)
}
}
Err(_) => Err(PercentError::NotANumber(input.to_string())),
}
}
fn main() {
for input in ["75", "half", "250"] {
match parse_percent(input) {
Ok(n) => println!("{n}%"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

75%
Error: 'half' is not a number
Error: 250 is more than 100
▶ Try it Yourself
  • 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.

Quick check

1. Why does a String error type fall short once a function can fail in more than one way?

2. What lets callers of withdraw react differently to InsufficientFunds versus NegativeAmount?

3. What does implementing Display for a custom error type let you do?

4. What do crates like thiserror and anyhow help with?

5. Why is comparing raw error message strings considered brittle?

Score: 0 / 5