Custom Error Types / thiserror
| Since: | Rust 1.0(2015) |
|---|
In Rust, you can define custom error types to represent application-specific errors in a type-safe way. The basic pattern is to define error variants using an enum and provide error messages by implementing the Display trait.
Syntax
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum AppError {
NotFound(String),
ParseError(String),
IoError(std::io::Error),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(s) => write!(f, "Not found: {}", s),
AppError::ParseError(s) => write!(f, "Parse error: {}", s),
AppError::IoError(e) => write!(f, "I/O error: {}", e),
}
}
}
impl Error for AppError {}
Error Design Patterns
| Pattern | Description |
|---|---|
| enum MyError { ... } | Defines error kinds as enum variants. |
| impl Display for MyError | Defines a user-facing error message (required). |
| impl Error for MyError {} | Marks the type as an error trait implementor (empty body is fine). |
| impl From<io::Error> for MyError | Enables automatic conversion from another error type (allows use of the ? operator). |
| Box<dyn Error> | A trait object used when you need to return multiple different error types. |
| type Result<T> = std::result::Result<T, MyError> | Defines a type alias for Result to omit the error type annotation. |
Sample Code
Pattern 1: enum custom error + From conversion + ? operator
sample_custom_error.rs
use std::fmt;
use std::error::Error;
use std::num::ParseIntError;
#[derive(Debug)]
enum CalcError {
DivisionByZero,
Overflow,
ParseError(ParseIntError),
}
impl fmt::Display for CalcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CalcError::DivisionByZero => write!(f, "Division by zero error"),
CalcError::Overflow => write!(f, "Overflow occurred"),
CalcError::ParseError(e) => write!(f, "Number parse error: {}", e),
}
}
}
impl Error for CalcError {}
impl From<ParseIntError> for CalcError {
fn from(e: ParseIntError) -> Self {
CalcError::ParseError(e)
}
}
type CalcResult<T> = Result<T, CalcError>;
fn divide(a: i32, b: i32) -> CalcResult<i32> {
if b == 0 {
Err(CalcError::DivisionByZero)
} else {
Ok(a / b)
}
}
fn parse_and_divide(a: &str, b: &str) -> CalcResult<i32> {
let a: i32 = a.parse()?;
let b: i32 = b.parse()?;
divide(a, b)
}
fn main() {
match divide(10, 2) {
Ok(result) => println!("10 / 2 = {}", result),
Err(e) => println!("Error: {}", e),
}
match divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
let cases = vec![
("20", "4"),
("abc", "4"),
("20", "0"),
];
for (a, b) in cases {
match parse_and_divide(a, b) {
Ok(result) => println!("{} / {} = {}", a, b, result),
Err(e) => println!("{} / {} error: {} ({:?})", a, b, e, e),
}
}
fn flexible_error(fail: bool) -> Result<i32, Box<dyn Error>> {
if fail {
Err("simple error".into())
} else {
Ok(42)
}
}
println!("{:?}", flexible_error(false));
println!("{:?}", flexible_error(true));
}
Compile with the following command:
rustc custom_error.rs
./custom_error
10 / 2 = 5
Error: Division by zero error
20 / 4 = 5
abc / 4 error: Number parse error: invalid digit found in string (ParseError(ParseIntError { kind: InvalidDigit }))
20 / 0 error: Division by zero error (DivisionByZero)
Ok(42)
Err("simple error")
Pattern 2: Error chaining with source()
Implementing the source() method from the Error trait lets you preserve the original error as part of a chain. This makes it easier to trace the root cause during debugging.
error_chain.rs
use std::fmt;
use std::error::Error;
use std::num::ParseIntError;
#[derive(Debug)]
struct ScoreError {
message: String,
source: ParseIntError,
}
impl fmt::Display for ScoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to parse score: {}", self.message)
}
}
impl Error for ScoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
fn parse_score(s: &str) -> Result<u32, ScoreError> {
s.parse::<u32>().map_err(|e| ScoreError {
message: format!("input: '{}'", s),
source: e,
})
}
fn main() {
let inputs = vec!["100", "abc", "200"];
for input in inputs {
match parse_score(input) {
Ok(score) => println!("score: {}", score),
Err(ref e) => {
println!("Error: {}", e);
if let Some(src) = e.source() {
println!(" caused by: {}", src);
}
}
}
}
}
Compile with the following command:
rustc error_chain.rs ./error_chain score: 100 Error: Failed to parse score: input: 'abc' caused by: invalid digit found in string score: 200
Pattern 3: An enum error that consolidates multiple error types
When an application can produce multiple kinds of errors, define an enum error with a From implementation for each source error type. This allows the ? operator to convert them automatically.
multi_error.rs
use std::fmt;
use std::error::Error;
use std::num::ParseIntError;
#[derive(Debug)]
enum ProfileError {
MissingField(String),
InvalidPower(ParseIntError),
}
impl fmt::Display for ProfileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProfileError::MissingField(s) => write!(f, "Missing field: {}", s),
ProfileError::InvalidPower(e) => write!(f, "Invalid power value: {}", e),
}
}
}
impl Error for ProfileError {}
impl From<ParseIntError> for ProfileError {
fn from(e: ParseIntError) -> Self {
ProfileError::InvalidPower(e)
}
}
fn parse_profile(name: &str, power_str: &str) -> Result<String, ProfileError> {
if name.is_empty() {
return Err(ProfileError::MissingField(String::from("name")));
}
let power: u32 = power_str.parse()?;
Ok(format!("{} (power: {})", name, power))
}
fn main() {
let data = vec![
("user_a", "95"),
("", "88"),
("user_b", "abc"),
];
for (name, power) in data {
match parse_profile(name, power) {
Ok(s) => println!("OK: {}", s),
Err(e) => println!("Error: {}", e),
}
}
}
Compile with the following command:
rustc multi_error.rs ./multi_error OK: user_a (power: 95) Error: Missing field: name Error: Invalid power value: invalid digit found in string
Notes
If you implement From<OtherError> on your custom error type, the ? operator will automatically convert errors for you. This lets you write clean, concise code even in functions that can produce multiple kinds of errors.
For production applications, the thiserror crate is available. Its #[derive(Error)] macro automatically generates the Display and Error implementations.
For basic Result operations, see Result::unwrap() / expect() / ? operator.
Common Mistakes
Mistake 1: Missing Display implementation causes println! to fail
If a custom error type does not implement fmt::Display, using println!("{}", e) results in a compile error.
ng_no_display.rs
#[derive(Debug)]
enum AppError {
NotFound,
}
fn main() {
let e = AppError::NotFound;
println!("{}", e);
}
Compile with the following command:
rustc ng_no_display.rs error[E0277]: `AppError` doesn't implement `std::fmt::Display`
Adding a Display implementation resolves the error.
ok_no_display.rs
use std::fmt;
#[derive(Debug)]
enum AppError {
NotFound,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound => write!(f, "Resource not found"),
}
}
}
fn main() {
let e = AppError::NotFound;
println!("{}", e);
}
Compile with the following command:
rustc ok_no_display.rs ./ok_no_display Resource not found
Mistake 2: Missing From implementation prevents use of the ? operator
For the ? operator to automatically convert an error, the corresponding From trait must be implemented. Without it, a compile error occurs.
ng_no_from.rs
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
ParseFailed,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "parse failed")
}
}
fn parse_power(s: &str) -> Result<u32, AppError> {
let n: u32 = s.parse()?;
Ok(n)
}
Compile with the following command:
rustc ng_no_from.rs error[E0277]: `?` couldn't convert the error to `AppError` = note: the trait `From<ParseIntError>` is not implemented for `AppError`
Implementing From<ParseIntError> resolves the error.
ok_no_from.rs
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
ParseFailed(ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::ParseFailed(e) => write!(f, "Parse failed: {}", e),
}
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::ParseFailed(e)
}
}
fn parse_power(s: &str) -> Result<u32, AppError> {
let n: u32 = s.parse()?;
Ok(n)
}
fn main() {
println!("{:?}", parse_power("95"));
println!("{:?}", parse_power("abc"));
}
Compile with the following command:
rustc ok_no_from.rs
./ok_no_from
Ok(95)
Err(ParseFailed(ParseIntError { kind: InvalidDigit }))
Mistake 3: Missing std::error::Error trait implementation
Functions that return Box<dyn Error> or use error chaining require the custom error type to implement the std::error::Error trait. Without it, a compile error occurs.
ng_no_error_trait.rs
use std::fmt;
#[derive(Debug)]
struct AppError;
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "app error")
}
}
fn get_error() -> Result<(), Box<dyn std::error::Error>> {
Err(Box::new(AppError))
}
Compile with the following command:
rustc ng_no_error_trait.rs error[E0277]: the trait bound `AppError: std::error::Error` is not satisfied
Adding impl std::error::Error for AppError {} resolves the error.
ok_no_error_trait.rs
use std::fmt;
#[derive(Debug)]
struct AppError;
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "app error")
}
}
impl std::error::Error for AppError {}
fn get_error() -> Result<(), Box<dyn std::error::Error>> {
Err(Box::new(AppError))
}
fn main() {
match get_error() {
Ok(_) => println!("OK"),
Err(e) => println!("Error: {}", e),
}
}
Compile with the following command:
rustc ok_no_error_trait.rs ./ok_no_error_trait Error: app error
If you find any errors or copyright issues, please contact us.