Rust Custom Error Types
Strings and library errors work for small scripts. Larger programs need errors that describe exactly what failed, so callers can respond differently to each case. A custom error type acts like a hospital triage tag: it names the problem, and each department knows what to do with it.
Defining an Error Enum
An enum lists every failure the program can produce. Each variant may carry extra details.
#[derive(Debug)]
enum AppError {
EmptyInput,
NotANumber(String),
TooLarge { value: i32, limit: i32 },
}
AppError
/ | \
EmptyInput NotANumber TooLarge
(none) (text) (value, limit)
Making Errors Printable with Display
The Display trait controls how an error appears when printed with {}. Write messages that a user can understand.
use std::fmt;
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::EmptyInput => write!(f, "No input was given"),
AppError::NotANumber(text) => write!(f, "'{text}' is not a number"),
AppError::TooLarge { value, limit } =>
write!(f, "{value} is larger than the limit {limit}"),
}
}
}Joining the Error Trait
The standard Error trait marks a type as a proper error. An empty implementation is enough once Debug and Display exist.
impl std::error::Error for AppError {}The marker unlocks compatibility with Box<dyn Error> and many third-party tools.
Using the Error
fn check(text: &str) -> Result<i32, AppError> {
if text.trim().is_empty() {
return Err(AppError::EmptyInput);
}
let value: i32 = text
.trim()
.parse()
.map_err(|_| AppError::NotANumber(text.to_string()))?;
if value > 100 {
return Err(AppError::TooLarge { value, limit: 100 });
}
Ok(value)
}
fn main() {
for input in ["42", "", "abc", "500"] {
match check(input) {
Ok(v) => println!("Accepted {v}"),
Err(e) => println!("Rejected: {e}"),
}
}
}
Accepted 42 Rejected: No input was given Rejected: 'abc' is not a number Rejected: 500 is larger than the limit 100
Automatic Conversion with From
The question mark operator converts errors through From. Implement From for each outside error you want to absorb, and the operator handles the translation.
use std::num::ParseIntError;
#[derive(Debug)]
enum ReadError {
Parse(ParseIntError),
Negative(i32),
}
impl From<ParseIntError> for ReadError {
fn from(e: ParseIntError) -> Self {
ReadError::Parse(e)
}
}
fn read_positive(text: &str) -> Result<i32, ReadError> {
let n: i32 = text.parse()?; // ParseIntError becomes ReadError
if n < 0 {
return Err(ReadError::Negative(n));
}
Ok(n)
}
text.parse() --Err(ParseIntError)--> From --> ReadError::Parse
Wrapping Several Sources
Real programs combine file errors, parse errors, and rule violations. One enum with a variant per source keeps everything in one place.
#[derive(Debug)]
enum ConfigError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
Missing(String),
}Design Tips
- Name variants after the problem, not the function that failed.
- Store only data that helps the caller decide what to do.
- Write messages in plain language without technical jargon.
- Keep one error enum per module or per application area.
Shortcut with Helper Crates
Writing Display and From code repeatedly becomes tedious. The thiserror crate generates both from short attributes, and the anyhow crate offers a flexible error type for application code. Learn the manual way first, because it explains what those tools produce.
Choosing a Strategy
| Situation | Suggested Approach |
|---|---|
| Quick script | Box<dyn Error> |
| Library other people use | Custom enum |
| Application with many error sources | Custom enum or anyhow |
Common Mistakes
Storing too much detail inside variants makes errors awkward to build and match. Keep only the facts a caller needs.
Forgetting the Debug derive stops the compiler from accepting the type as an error. Both Debug and Display are required for the Error trait.
Writing vague messages such as "failed" frustrates users. A strong message names the problem and, when possible, hints at the fix.
Practice Ideas
- Create a
LoginErrorenum with variants for a wrong password, a locked account, and an unknown user. - Implement
Displayso each variant prints a friendly sentence. - Add a
Fromimplementation forParseIntErrorand use?inside a function.
Testing Errors
Custom enums make tests precise. A test can check that a function returns Err(AppError::EmptyInput) for blank text, which proves the exact failure path instead of any failure at all.
Documenting Errors
Public functions should document which errors they return. A short section in the doc comment lists each variant and the situation that triggers it. Callers then write correct handling code without reading the function body.
Key Points
- A custom enum names each failure the program can produce.
- Implement
Displayfor readable messages andErrorfor compatibility. - Implement
Fromso?converts outside errors automatically. - Helper crates reduce repeated code once the basics are clear.
