Easy error handling in Rust
Creating new error types
Let's wrap std library errors with our Errors
use std::fmt;
use std::fmt::Debug;
use std::num::{ParseIntError, ParseFloatError};
#[derive(Debug)]
enum MyErrors {
Error1,
Error2
}
We also need to implement fmt::Display for our Errors.
impl fmt::Display for MyErrors {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MyErrors::Error1 => write!(f, "my first error"),
MyErrors::Error2 => write!(f, "my second error"),
}
}
}
Let's implement our own function to test this
fn test_error1() -> Result<(), MyErrors> {
let _ = "a".parse::<i32>()?;
let _ = "a".parse::<f64>()?;
Ok(())
}
fn test_error2() -> Result<(), MyErrors> {
let _ = "1".parse::<i32>()?;
let _ = "a".parse::<f64>()?;
Ok(())
}
The above one raises error, because Rust doesn't know how to convert ParseIntError to MyErrors. We need to implement them using From trait.
impl From<ParseIntError> for MyErrors {
fn from(err: ParseIntError) -> MyErrors {
MyErrors::Error1
}
}
impl From<ParseFloatError> for MyErrors {
fn from(err: ParseFloatError) -> MyErrors {
MyErrors::Error2
}
}
You can also avoid the above step and convert it explicitly using
let _ = "a".parse::<i32>().map_err(|_| MyErrors::Error1)?;
But, implementing From once and using it everywhere is less verbose.We can use these in our main function which returns our error type on failure.
fn main() -> Result<(), MyErrors> {
//test_error1()?;
if let Err(e) = test_error1() {
println!("Error message: {}", e)
}
if let Err(e) = test_error2() {
println!("Error message: {}", e)
}
Ok(())
}
If we want to use std::error::Error
in main, we need to implement it for MyErrors
impl std::error::Error for MyErrors{}
Then we can use
fn main() -> Result<(), Box<dyn std::error::Error>> {
...
}
Full working example using everything above. Remember if you don't want infallible main, you can just print the error message in main if any, otherwise use ? operator.
Wrapping underlying errors
We can create our error type that can directly wrap underlying error and print backtrace.
#[derive(Debug)]
enum MyErrors {
Error1(ParseIntError),
Error2(ParseFloatError)
}
// Implement Display
impl fmt::Display for MyErrors {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
MyErrors::Error1(..) => write!(f, "my first error"), // wrap ParseIntError
MyErrors::Error2(..) => write!(f, "my second error"), // wrap ParseFloatError
}
}
}
We need to implement From for converting error types from one to another
// convert ParseIntError to MyErrors::Error1
impl From<ParseIntError> for MyErrors {
fn from(err: ParseIntError) -> MyErrors {
MyErrors::Error1(err)
}
}
// convert ParseFloatError to MyErrors::Error1
impl From<ParseFloatError> for MyErrors {
fn from(err: ParseFloatError) -> MyErrors {
MyErrors::Error2(err)
}
}
We can use the same test functions as above [1] and we need backtrace. For backtrace, instead of empty impl std::error::Error
for our error type, we can implement source.
impl std::error::Error for MyErrors {
// if you want source of the error ----
// In our case display error message related to
// ParseIntError or ParseFloatError
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
MyErrors::Error1(ref e) => Some(e),
MyErrors::Error2(ref e) => Some(e),
}
}
}
and we have our main function like this
fn main() -> Result<(), Box<dyn std::error::Error>> {
// test_error1()?;
if let Err(e) = test_error1() {
println!("Error message: {}", e);
if let Some(source) = e.source() {
println!(" Caused by: {}", source);
}
}
Ok(())
}
We get this output
Error message: my first error
Caused by: invalid digit found in string
Playground for above -- with boilerplate
To create our own error type, we implemented, From conversions, std::error::Error and it's methods. In order to avoid all these, we can use a trait called thiserror
#[derive(thiserror::Error, Debug)]
enum MyErrors {
#[error("my first error")]
Error1(#[from] ParseIntError),
#[error("my second error")]
Error2(#[from] ParseFloatError)
}
The above implements custom error type with our message, from conversions and backtrace.
Playground for above -- using thiserror
Both of these, produce the same output. Using thiserror
reduced the code size by half in our example
Top comments (0)