Creating Custom Error Types in Rust

· todoroki's blog


Introduction #

This guide will walk through how to create custom error types in Rust, wrap standard library errors, and implement the necessary traits.

Basic Custom Error Type #

Let's start by creating a basic error enum:

1use std::fmt;
2use std::fmt::Debug;
3use std::num::{ParseIntError, ParseFloatError};
4
5#[derive(Debug)]
6enum MyErrors {
7    Error1,
8    Error2
9}

We need to implement fmt::Display for our custom errors:

1impl fmt::Display for MyErrors {
2    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3        match *self {
4            MyErrors::Error1 => write!(f, "my first error"),
5            MyErrors::Error2 => write!(f, "my second error"),
6        }
7    }
8}

Testing Our Error Type #

Let's implement test functions:

 1fn test_error1() -> Result<(), MyErrors> {
 2    let _ = "a".parse::<i32>()?;
 3    let _ = "a".parse::<f64>()?;
 4    Ok(())
 5}
 6
 7fn test_error2() -> Result<(), MyErrors> {
 8    let _ = "1".parse::<i32>()?;
 9    let _ = "a".parse::<f64>()?;
10    Ok(())
11}

These functions will raise errors because Rust doesn't know how to convert ParseIntError to MyErrors. We need to implement the From trait:

 1impl From<ParseIntError> for MyErrors {
 2    fn from(err: ParseIntError) -> MyErrors {
 3        MyErrors::Error1
 4    }
 5}
 6
 7impl From<ParseFloatError> for MyErrors {
 8    fn from(err: ParseFloatError) -> MyErrors {
 9        MyErrors::Error2
10    }
11}

Alternatively, you can explicitly convert errors:

1let _ = "a".parse::<i32>().map_err(|_| MyErrors::Error1)?;

But implementing From once and using it everywhere is less verbose.

Using in Main Function #

We can use our error type in the main function:

1fn main() -> Result<(), MyErrors> {
2    if let Err(e) = test_error1() {
3        println!("Error message: {}", e)
4    }
5    if let Err(e) = test_error2() {
6        println!("Error message: {}", e)
7    }
8    Ok(())
9}

Implementing std::error::Error #

If we want to use std::error::Error in main, we need to implement it for our error type:

1impl std::error::Error for MyErrors{}

Then we can use:

1fn main() -> Result<(), Box<dyn std::error::Error>> {
2   // ...
3}

Wrapping Underlying Errors #

We can create an error type that directly wraps underlying errors and prints a backtrace:

 1#[derive(Debug)]
 2enum MyErrors {
 3    Error1(ParseIntError),
 4    Error2(ParseFloatError)
 5}
 6
 7// Implement Display
 8impl fmt::Display for MyErrors {
 9    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
10        match *self {
11            MyErrors::Error1(..) => write!(f, "my first error"), // wrap ParseIntError
12            MyErrors::Error2(..) => write!(f, "my second error"), // wrap ParseFloatError
13        }
14    }
15}

Implement From for converting error types:

 1// convert ParseIntError to MyErrors::Error1
 2impl From<ParseIntError> for MyErrors {
 3    fn from(err: ParseIntError) -> MyErrors {
 4        MyErrors::Error1(err)
 5    }
 6}
 7
 8// convert ParseFloatError to MyErrors::Error2
 9impl From<ParseFloatError> for MyErrors {
10    fn from(err: ParseFloatError) -> MyErrors {
11        MyErrors::Error2(err)
12    }
13}

For backtrace, implement the source method:

1impl std::error::Error for MyErrors {
2    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
3        match *self {
4            MyErrors::Error1(ref e) => Some(e),
5            MyErrors::Error2(ref e) => Some(e),
6        }
7    }
8}

Main function with error handling:

1fn main() -> Result<(), Box<dyn std::error::Error>> {
2    if let Err(e) = test_error1() {
3        println!("Error message: {}", e);
4        if let Some(source) = e.source() {
5            println!(" Caused by: {}", source);
6        }
7    }
8    Ok(())
9}

Output:

Error message: my first error
   Caused by: invalid digit found in string

Simplifying with thiserror #

To avoid all the boilerplate, we can use the thiserror crate:

1#[derive(thiserror::Error, Debug)]
2enum MyErrors {
3    #[error("my first error")]
4    Error1(#[from] ParseIntError),
5    #[error("my second error")]
6    Error2(#[from] ParseFloatError)
7}

This single declaration implements custom error type with our message, from conversions, and backtrace functionality.

Both approaches produce the same output, but using thiserror reduces the code size by half in our example.