DEV Community

Abhishek Gupta for ITNEXT

Posted on • Updated on

Rust: Error handling

This blog covers error handling in Rust with simple examples.

Rust does not have exceptions - it uses the Result enum which is a part of the Rust standard library and it is used to represent either success (Ok) or failure (Err).

enum Result<T,E> {
    Ok(T),
    Err(E),
}
Enter fullscreen mode Exit fullscreen mode

When you are writing functions and need to handle potential problems, you can exit the program using the panic! (macro) - this should be rare

panic

fn gen1() -> i32 {
    let mut rng = rand::thread_rng();
    let num = rng.gen_range(1, 10);
    if num <= 5 {
        num
    } else {
        panic!("{} is more than 5", num);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we call panic! if the random number is more than 5

To make our intentions clearer, we can return a Result

Using Result

fn gen2() -> Result<i32, String> {
    let mut rng = rand::thread_rng();
    let num = rng.gen_range(0, 10);
    if num <= 5 {
        Ok(num)
    } else {
        Err(String::from(num.to_string() + " is more than 5"))
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, if the number is less than equal to 5, we return the Ok variant else, we return an Err with a String message

we could have used an Option return type in this case, but in this case, we are using result since a number more than 5 is an erroneous scenario (in our fictitious use case)

Now we have a method which returns a Result, let's explore how a caller would use this method. There are a few options:

  • Use panic!
  • Acknowledge the problem and handle it
  • Pass it on

panic (again!)

This is the easiest but most often, the least desirable way to handle problems. Just panicking is a terrible idea. We can make it a little better, if not ideal

use expect

expect is a shortcut - it is a method available on Result which returns the content from the Ok variant if its available. Otherwise, it panics with the passed in message and the Err content

fn caller() -> String {
    let n = gen2().expect("generate failed!");
    n.to_string()
}
Enter fullscreen mode Exit fullscreen mode

use unwrap

unwrap is similar to expect apart from the fact that it does not allow you to specify a custom message (to be displayed on panic!)

fn caller() -> String {
    let n = gen2().unwrap();
    n.to_string()
}
Enter fullscreen mode Exit fullscreen mode

We can do a little better with unwrap_or() and use it to return a default value in case of an Err

fn caller() -> String {
    let zero = gen2().unwrap_or(0);
    zero.to_string()
}
Enter fullscreen mode Exit fullscreen mode

In this case, we return "0" (String form) in case of an error

Handle the problem

fn caller() -> String {
    let r = gen2();
    match r {
        Ok(i) => i.to_string(),
        Err(e) => e,
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, we match on the Result: return a String (using i.to_string()) or return the Err content (which also happens to be a String in this case)

Not my problem.. pass it on

What if you did not want to handle the problem and need to propagate it to the caller (in a slightly different way)?

Instead of returning an explicit String, you can return a Result<String,String> where Ok variant will contain a String result and an Err variant will (also) contain a String result explaining with error details

note that a String has been used to represent error (Err) as well. This is just to keep things simple. Typically, you would use a special error type e.g. std::io:Error etc.

Here is one way:

fn caller() -> Result<String, String> {
    let r = gen2();
    match r {
        Ok(i) => Ok(i.to_string()),
        Err(e) => Err(e),
    }
}
Enter fullscreen mode Exit fullscreen mode

We change the return type to Result<String, String>, invoke the function and match (which returns a Result<i32,String>). All we need is to make sure we return the String conversion of i32. So we match against the returned Result - if its Ok, we return another Ok which contains the String version of i32 i.e. Ok(i) => Ok(i.to_string()). In case of an error, we simply return it as is

? operator

We can simplify this further by using the ? operator. This is similar to the match process wherein, it returns the value in the Ok variant of the Result or exits with by returning Err itself. This is how its usage would look like in our case:

fn caller() -> Result<String, String> {
    let n = gen2()?;
    Ok(n.to_string())
}
Enter fullscreen mode Exit fullscreen mode

The method is called - notice the ? in the end. All it does it return the value in Ok variant and we return its String version using Ok(n.to_string()). This takes care of the one half (the happy path with Ok) of the Result - what about the other half (Err)? Like I mentioned before, with ? its all taken care of behind the scenes, and the Err with its value is automatically returned from the function!

Please note that the ? operator can be used if you a returns Result or Option or another type that implements std::ops::Try

What if the Err type was different?
You can/will have a situation where you're required to return a different error type in the Result. In such a case, you will need to implement std::convert::From for your error type. e.g. this won't work since Result<String, NewErr> is the return type

...
//new error type
struct NewErr {
    ErrMsg: String,
}
...
fn caller5() -> Result<String, NewErr> {
    let n = gen2()?;
    Ok(n.to_string())
}
Enter fullscreen mode Exit fullscreen mode

You need to tell Rust how to convert from String to NewErr in this case since the return type for the gen2() function is Result<String,String>. This can be done as such:

impl std::convert::From<String> for NewErr {
    fn from(s: String) -> Self {
        NewErr { ErrMsg: s }
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)