All about Error Handling In Rust — With Examples, Define custom errors and Boxed errors with Box

When I started learning Rust lang, the first roadblock was it’s memory management and ownership rules. The next one that really got to me was error handling. As developers, at some point in writing a program, it becomes essential to handle all kinds of errors, and deal with them in usual program flow.

It took me a while to understand errors and result types in rust. In this article, I'm going to share working examples on how to handle errors, and how can we work with error handling in a nice clean manner with Rust custom error types.

I encourage readers to try this out in their developer environments and follow along. This really helps in understanding these concepts.

Recoverable and Unrecoverable Errors

Like many other programming languages, there are two categories of errors in Rust. Recoverable and Unrecoverable errors. Recoverable errors allow us the ability to act and handle any unexpected scenarios in the program gracefully — in case something goes wrong during normal flow of the program. Unrecoverable errors , on the other hand involve terminating the program immediately without giving a chance to handle them. An example of this would be panics and something that we didn’t intend to do . ( Refer to this to see an example. )

Rust doesn’t have Exceptions like other languages. All recoverable errors are handled using the Result Type which is an enum. It returns a value on success or an error in case of an error. A method that can return an error can be of type Result<T,E> where T can be any return type, and E the error that we’re trying to return. To understand this better, let’s understand the Result type first.

Result<T,E> Type in Rust

The result type in rust is defined as an Enum with two variants.

enum Result<T, E> {  
    Ok(T),  
    Err(E),  
}

Result can take two forms. Ok(T) denotes that the operation is successful and Err<E> variant denotes any form of error. Let’s take an example.

Let’s say we want to compute fare for a ride sharing platform. This fare may be dependent on the distance, current weather conditions, and the demand. It’s not a straightforward computation just proportional to the distance. There can be many variables and service calls before a fare is calculated.

The method to compute a fare for any ride may result in errors in the form of API call failures, database queries, unknown errors and we want our program to handle these gracefully. For the purposes of this demo, let’s abstract this into a function and return a result type — the simplest example first. This will return the Ok variant if the computation is successful. And it returns an error string if the call fails. Below is our function.

fn compute_dynamic_fare(distance : f32) -> Result<f64, String> {  
    if distance == 0.0 {  
        return Err("invalid input. distance must be greater than zero.".to_owned())  
    }  
    Ok((distance as f64)  * 3.0)  
}

fn main() {  
    let distance : f32 = 0.0 ;  
    let res = compute_dynamic_fare(distance) ;  
    match res  {  
        Ok(fare) => {  
            println!("fare computed  = {} " , fare)  
        }  
        Err(msg) => {  
            println!("error computing fare : {}", msg)  
        }  
    }  
}

Notice how we use Result type to convey our computation outcome. In this case, we throw an error if the distance input is zero. There may be many other cases which may cause the computation to fail. We can use this generic Result type to propagate Result to our callers.

Defining Custom Error Types

As you may have noticed, there is a problem with the above code. The return type for our error result was a string — the error message. This is not a good practice while working with real world programs. We should have errors that are typed and must be conveyed in a structured manner. This allows our code to be cleaner and facilitates better design.

Let’s take the above program and try to introduce a custom type to replace our string error.

To write custom error types in Rust, we simply implement the Error trait in the standard lib. The source for the Error trait from Rust standard library looks something like this.

pub trait Error: Debug + Display {  
    fn source(&self) -> Option<&(dyn Error + 'static)>   
}

In order to create a custom error type, we need to implement the Display trait , and an optional source method. At the time of authoring this article, there is a default implementation for the source method within this trait. Let’s define an error type and implement our Display Trait.

#[derive(Debug)]  
struct InvalidDistanceInput {  
    // keep track of what value triggered this .  
    triggered_by_distance_value: f32  
}  
  
impl Error for InvalidDistanceInput {  
  
}  
impl Display for InvalidDistanceInput {  
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {  
        write!(f, "invalid input. distance must be greater than zero. Provided value {}.",&self.triggered_by_distance_value)  
    }  
}  
  
impl InvalidDistanceInput {  
    fn new(distance : f32) -> Self{  
        return InvalidDistanceInput{triggered_by_distance_value : distance}  
    }  
}  
  
fn compute_dynamic_fare(distance : f32) -> Result<f64, InvalidDistanceInput> {  
    if distance <= 0.0 {  
        return Err(InvalidDistanceInput::new(distance))  
    }  
    Ok((distance as f64)  * 3.0)  
}

We define a struct for holding our error. And this becomes are Error object when we want our method to return errors. We first implement the Error traitfor our struct, and implement a Displaytrait as well. This is all that is required at the very minimum to define custom error types.

Notice how we changed our error from a string to a typed struct with inputs on what went wrong. We hold the input that caused an error for printing or debugging purposes. The display trait uses this data to render error outputs.

What if we wanted Multiple errors returned from Fare calculator

Until now, we’ve seen how to create a single error type and returning it as a result. A question that comes next is what if there exist multiple types of errors that may be thrown by our fare calculator ? In many real world use cases, there may be multiple types of error involved. A database may throw error while connecting, opening a database, and writing / reading the data. How do we handle these in our fare calculator ? In the next few sections, we’ll see how to handle this using two approaches building on our example and simplifying our errors.

Using Error Type Enum

An improvement to our program above is to define an enum of errors and have specific enum variant for each type of error. This allows us to better organize our code. Refer to the code below.

#[derive(Debug)]  
enum DistanceCalculatorErrorReason {  
    // pass the distance that was used as input to this enum variant.   
    InvalidInput(f32),  
    // store the status code within the enum value.   
    WeatherServiceUnreachable(i32)  
}  
  
#[derive(Debug)]  
struct FareCalculationError {  
    reason : DistanceCalculatorErrorReason  
}

In this case, we defined two errors, one for Invalid Distance input, and the other one for an internal server error when weather service is unreachable. We removed the distance field from our struct and wrapped any specific inputs within the enum. We also renamed our error to be more generic in nature.

Next, we update our Display method to handle all cases of this error and print out the output for us. This is an important step in formatting errors.

impl Display for FareCalculationError {  
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {  

      match self.reason {  
          DistanceCalculatorErrorReason::InvalidInput(distance) => {  
              write!(f, "invalid input. distance must be greater than zero. Provided value {}.",distance)  
          }  
          DistanceCalculatorErrorReason::WeatherServiceUnreachable(status) => {  
              write!(f, "Unable to compute fare. Service unreachable with status {}.",status)  
          }  
      }  

  }  
}

All this method does is to take each error type and output the appropriate error message to the writer.

Our calling method and main method can be refactored to look like this .

fn compute_dynamic_fare(distance : f32) -> Result<f64, FareCalculationError> {  
  if distance <= 0.0 {  
      return Err(FareCalculationError::new(DistanceCalculatorErrorReason::InvalidInput(distance)))  
  }  
  Ok((distance as f64)  * 3.0)  
}  

fn main() {  
  let distance : f32 = 0.0 ;  
  let res = compute_dynamic_fare(distance) ;  
  match res  {  
      Ok(fare) => {  
          println!("fare computed  = {} " , fare)  
      }  
      Err(msg) => {  
          println!("error computing fare : {}", msg)  
      }  
  }  
}

This is much cleaner than panicking or printing error messages on the console, and it can handle multiple error variants for fare calculator. But it is a little verbose as well. What if we could reduce the number of lines it takes to write and create custom errors ? And what if we could supply dynamic error messages ?

For this purpose, there is an open source library called thiserror which facilitates error handling by using a set of macros. It really reduces the number of lines we need to write for error definitions. Let’s take a look on how to use this library with our Error Type defined above.

thiserror Library and Custom Errors

We’ll refactor our code to use a 3rd party open source library to work with errors in Rust. Add the following to Cargo.toml

[dependencies]  
thiserror = "1.0.40"

Now remove all lines pertaining to the definition of our FareCalcuationError object, and add the following instead.

use thiserror::Error;  
  
#[derive(Error, Debug)]  
pub enum FareCalculationError {  
    #[error("invalid input. distance must be greater than zero. Provided value {input_distance:?}")]  
    InvalidInput{ input_distance : f32 },  
    #[error("Unable to compute fare. Service unreachable with status `{0}`.")]  
    WeatherServiceUnreachable(i32),  
}

Instead of defining a struct, implementing display and error traits, and handling various types of errors, we just defined an error object which has variants corresponding to each error type.

We define the error message within the #error() macro and can provide arguments to customize the error message. This allows us to customize almost everything about an error and easily write errors in Rust. There are other libraries out there, but I just prefer this one. Try exploring this and creating your own types. Notice how it needed only 4 lines of code to define and handle all errors we were writing a lot of code for. There may be cases where we have to write custom methods, but for most cases, this should be sufficient.

Using Boxed errors.

One last thing i want to share before we wrap up is the use of Boxed errors.

In most of the cases we would want to standardize our error types so that a single method returns one particular error type, and the stack propagated for any underlying causes.

But sometimes, we may not know the type of the error at Runtime. In such cases, we can use a **Box<dyn Error>** to catch any error and display error details. This may be useful also when we want to create a list of errors for which we dont’ know the size or type at compile time.

Another use case for using Box is when we don’t know the type of error a Result may throw. Imagine in our fare calculator we had multiple types of errors which we don’t want to handle or didn’t know how to handle. Our goal is to just propagage these errors to the calling client and let them handle it. We can use **Box < dyn Error>** in such cases. Here’s an example of our program modified to use Generic Error result types. Please note the return types here.

#[derive(Error, Debug)]  
pub enum FareCalculationError {  
  #[error("invalid input. distance must be greater than zero. Provided value {input_distance:?}")]  
  InvalidInput{ input_distance : f32 },  
  #[error("Unable to compute fare. Service unreachable with status `{0}`.")]  
  WeatherServiceUnreachable(i32),  
}  

fn compute_dynamic_fare(distance : f32) -> Result<f64, Box<dyn Error>> {  
  if distance <= 0.0 {  
      return Err(FareCalculationError::InvalidInput {input_distance: distance}.into())  
  }  
  Ok((distance as f64)  * 3.0)  
}  
fn main() -> Result<() , Box<dyn Error>> {  
  let distance : f32 = 0.0 ;  
  let res = compute_dynamic_fare(distance) ;  
  match res  {  
      Ok(fare) => {  
          println!("fare computed  = {} " , fare)  
          Ok(())  
      }  
      Err(msg) => {  
          Err(msg)  
      }  
  }  
}

Also, notice that the main function in Rust can return a Result type too. The value is empty, but the second parameter can be any error within the program.

The End !

What we’ve seen in this article is how to create errors, handle errors and produce programs that have proper error handling. We’ve created errors in a few common ways and covered all the basics that are required to write nice, easy to read, and clean errors.

Hopefully error handling makes sense now. Learning error handling has helped a lot while writing Rust Programs. Don’t forget to implement this and try it. I bet you wont’ forget anything about errors from now on :)

Please stay tuned and don’t forget to follow me for more :)