DEV Community

toyster
toyster

Posted on

Error handling in Rust

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 
}
Enter fullscreen mode Exit fullscreen mode

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"), 
        } 
    } 
}

Enter fullscreen mode Exit fullscreen mode

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(()) 
}
Enter fullscreen mode Exit fullscreen mode

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
    } 
}
Enter fullscreen mode Exit fullscreen mode

You can also avoid the above step and convert it explicitly using

let _ = "a".parse::<i32>().map_err(|_| MyErrors::Error1)?;
Enter fullscreen mode Exit fullscreen mode

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(()) 
}
Enter fullscreen mode Exit fullscreen mode

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

impl std::error::Error for MyErrors{} 
Enter fullscreen mode Exit fullscreen mode

Then we can use

fn main() -> Result<(), Box<dyn std::error::Error>> {
   ...
}
Enter fullscreen mode Exit fullscreen mode

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 
        } 
    } 
}

Enter fullscreen mode Exit fullscreen mode

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) 
    } 
}
Enter fullscreen mode Exit fullscreen mode
// convert ParseFloatError to MyErrors::Error1 
impl From<ParseFloatError> for MyErrors { 
    fn from(err: ParseFloatError) -> MyErrors { 
        MyErrors::Error2(err) 
    } 
}
Enter fullscreen mode Exit fullscreen mode

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), 
        } 
    } 
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

We get this output

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

Enter fullscreen mode Exit fullscreen mode

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) 
}
Enter fullscreen mode Exit fullscreen mode

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)