Sivaram Konanki

June 11, 2021

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 [1]

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