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 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 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, 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.
Practice
Section titled “Practice”1. Login errors
Section titled “1. Login errors”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 emptyError: password must be at least 8 characters2. Errors that carry details
Section titled “2. Errors that carry details”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 numberError: 250 is more than 100- 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.
Quick check
Section titled “Quick check”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
Read more
Section titled “Read more”- Defining an error type — Rust by Example: a short, runnable example of a custom error type with
Display. - Wrapping errors — Rust by Example: the next step, an error enum that wraps other errors and works with
?throughFrom. std::error::Error— std docs: the standard trait that “real” error types implement, and what itssource()method is for.- To panic! or Not to panic! — The Rust Book: guidelines for when to return an error and when to stop the program.
- thiserror — docs.rs: the crate that derives
DisplayandErrorfor your error enums, so you write much less code.