If Rust does not have exceptions, then how do you handle errors in Rust?

Photo by Jake Givens on Unsplash

If Rust does not have exceptions, then how do you handle errors in Rust?

Don't panic! Rust has Results

·

4 min read

"Rust does not have exceptions".

This sounds very interesting, but what does it mean? Rust handles errors in a sophisticated manner, not like try-catch block type error handling. There are two approaches either panic for unrecoverable errors or use Result Enum for recoverable error. We could panic whenever we come across something messed up, a bug like divide by zero. Its not a crash, it means Rust walk back up the stack and clean up the data from each function it encounters. It’s more like a RuntimeException in Java or a std::logic_error in C++. Lets focus on Result as it the topic of discussion. The use of Result enum in error handling gives you a fresh perspective, potential best practices, and avoiding neglect. The use Result as a return type of functions forces you to think in a non-lazy approach about all the places/functions an error could occur and makes decisions like:

  • Propagating errors to caller functions.

  • Discarding errors or reacting to specific errors.

You end up thinking of error handling more in Rust as you need to decide if the function is fallible because the return type of function depends on it.

Let's first discuss the Result<T,E> enum here.

Result<T,E>

Result is a built-in enum in the Rust standard library with two variants Ok(T) and Err(E).

Ok(T): returns the success statement of type T.

Err(E): returns the error statement of type E.

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

Result must be used as a return type for a function that can encounter error situations. Such functions can return an Ok value in case of success or an Err value in case of an error. Using Result as a function return type can be used to return various kinds of success and error codes to let the calling function decode the execution state of the called function.

fn abc(a:f64)->Result<f64,bool>{
        if a ==20.0{
            println!("Going good");
            Ok(a/2.0)
        }
            else if a<20.0 {
                Ok(a)
            }
        else {
            println!("this is error");
            Err(false)
        }
    }
match abc(10.0){
        Ok(T)=>{
            println!("What are you talking about? Here is no error!!")
        }
        Err(E)=>{
            println!("Finally caught a error!")
        }
    }

match looks like a try/catch block. As we received the Result enum, it requires some processing of Result <T,E> if we want to handle it instead of passing it to the caller. There are various methods that Result <T,E> provides to handle it. For example

pub fn unwrap_or(self, default: T) -> T

This returns the contained Ok value or a provided default.

let r = abc(40.0).unwrap_or(0.0);
println!("The value of r is {}",r);

? Operator

Rust has ? operator that helps to propagate the errors to the caller function. Any functions that return a Result, we can add ? like

let r = abc(20.0)?;

This unwraps the Result to get the value inside. In case of an error, it immediately returns from the enclosing functions, passing the error result up the call chain. This ? looks pretty cool, isn't it?

How to deal with multiple errors types

Let's say we are using ? in multiple lines that have different error types in Result.

    fn abc(a:f64)->Result<f64,bool>{
        let x = x(a)?;
        let y = y(a)?;
        Ok(0.0)
    }
    fn x(a:f64)->Result<f64,String>{
        Err("Error in x".parse().unwrap())
    }
    fn y(a:f64)->Result<f64,bool>{
        Err(false)
    }
error[E0277]: `?` couldn't convert the error to `bool`
  --> src\main.rs:42:21
   |
41 |     fn abc(a:f64)->Result<f64,bool>{
   |                    ---------------- expected `bool` because of this
42 |         let x = x(a)?;
   |                     ^ the trait `From<String>` is not implemented for `bool`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = help: the following other types implement trait `FromResidual<R>`:
             <Result<T, F> as FromResidual<Result<Infallible, E>>>
             <Result<T, F> as FromResidual<Yeet<E>>>
   = note: required because of the requirements on the impl of `FromResidual<Result<Infallible, String>>` for `Result<f64, bool>`

The problem is two different types of errors types String and bool. We can deal with this using the Generic type.

type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;

! operator Result<T, !>

Often you need a function return type as Result to match trait signatures even though the function never fails. Rust provides the never type, written with the ! syntax. The never type represents a value that can never be generated.

Useful Resources

Did you find this article valuable?

Support Siddharth by becoming a sponsor. Any amount is appreciated!