DEV Community

nikhilraojl
nikhilraojl

Posted on

Intro to errors in Rust

Rust unlike many other programming languages doesn't have exceptions to handle errors. What does it mean by handling an error? Let's consider a very simple program in Python that converts a string into an integer with one success case and one that can fail

Error handling in Python

num_str = "10"
parsed = int(num_str)
print("completed")

# output
10
Enter fullscreen mode Exit fullscreen mode

The above conversion from string to an integer executes successfully

name_str = "john"
parsed = int(name_str)
print("completed")

# output
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'john'
Enter fullscreen mode Exit fullscreen mode

The above conversion fails with a ValueError exception. If we observe output the print statement after the exception is not executed. An exception not handled properly will halt the program, imagine such a piece of code halting a production server and needing a restart. Below is an example with a try-except block in Python, the exception is handled gracefully and will not halt the program.

name_str = "john"
try:
    parsed = int(name_str)
except ValueError as e:
    print(e)
print("completed")

# output
invalid literal for int() with base 10: 'john'
completed
Enter fullscreen mode Exit fullscreen mode

This way, in Python, we can catch exceptions. Once we catch them we can then convert them to a different exception, log the exception or just ignore it.

Error handling in Rust

Coming back to Rust, let's code up a similar string to an integer conversion program.

fn main() {
    let num_str: String = "10".to_owned();
    let _parsed_string: i32 = num_str.parse::<i32>().unwrap();
    println!("Completed");
}
Enter fullscreen mode Exit fullscreen mode

TIP: You can try this directly on rust playground https://play.rust-lang.org and see the output for yourselves

Here, we are trying to parse a String into i32 type and the program executes successfully. We will get to why .unwrap() is needed later. For now, we just want the code to compile and the Rust compiler won't allow it if we don't at least use unwrap. Now, what happens if we try and run this program

fn main() {
    let num_str: String = "john".to_owned();
    let _parsed_string: i32= num_str.parse::<i32>().unwrap();
    println!("Completed");
}

# output
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:3:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Enter fullscreen mode Exit fullscreen mode

The program panics during execution with a ParseIntError as it doesn't know how to convert characters in john to an integer value. Fair enough, but how do we handle such errors gracefully and not panic?

The Result enum

If we take a look at the parse method on a String type, we see that the return type is a Result.

pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
    ...
}
Enter fullscreen mode Exit fullscreen mode

The Result is an enum with two variants

  • Ok(T) -> representing success and its value
  • Err(E) -> representing error with error value

Ok, but how does a Result enum help us in error handling? Simple, every time there is a Result type returned from a function we need to handle both its variants, be it success or error. The Rust compiler shouts at us if we don't(try it for yourself in the Rust playground). If it is a success, we continue with the value from Ok variant and if there is an error we can handle it with Err variant. Below is an example of handling the error by just printing out a message

use std::num::ParseIntError;

fn main() {
    let num_str: String = "john".to_owned();
    let parsed_result: Result<i32, ParseIntError> = num_str.parse::<i32>();
    match parsed_result {
        Ok(parsed_string) => println!("Successfully parsed {}", parsed_string),
        Err(_) => println!("There was an error parsing the input"),
    }
    println!("Completed");
}

# output
There was an error parsing the input
Completed
Enter fullscreen mode Exit fullscreen mode

If we observe the output, ParseIntError is now successfully handled by our program and the rest of the code is executed as well without panicking. This is what we were aiming for

The unwrap method

So what is the unwrap thing mentioned earlier? If we again take a look at our first Rust example in this blog converting 10 into an integer,

    let _parsed_string: i32 = "10".to_owned().parse::<i32>().unwrap();
Enter fullscreen mode Exit fullscreen mode

we did not use any match statement to handle the Result enum, instead, we used the unwrap method. Yet the code compiled and returned the output successfully.

If the output from a function after its execution is of the Ok(T) variant the value from type T is consumed and the program continues as expected. But if the output from a function is of the Err(E) variant the current function will panic.

If we take a look at our second code example,

    let _parsed_string: i32 = "john".to_owned().parse::<i32>().unwrap();
Enter fullscreen mode Exit fullscreen mode

where we tried to parse the characters in "john" into an integer using unwrap and our program panicked.

Why do we use unwrap if the program can still panic? unwrap is used to quickly handle any Result types only thinking about valid/correct cases and ignoring handling errors. Although heavy use of unwrap is discouraged, it is very handy during development. We can always go back and handle Result types properly at a later stage.

The ? operator

Imagine a case where we are calling a function that returns a Result but we don't want to handle its Err variant manually(matching and returning a new Err), instead, we would just like to forward the Err to the calling function.

Let's consider the below example where we try to parse all elements in a Vec and return its total sum to any function that calls parse_vec_and_sum.

use std::num::ParseIntError;

fn main() {
    let strings_vec: Vec<&str> = vec!("1", "2", "3");

    let parsed_result = parse_vec_and_sum(strings_vec);
    match parsed_result {
        Ok(parsed_string) => println!("Successfully parsed vec to {}", parsed_string),
        Err(_) => println!("There was an error parsing the input"),
    }
    println!("Completed");
}

fn parse_vec_and_sum(v: Vec<&str>) -> Result<i32, ParseIntError> {
    let mut sum: i32 = 0;
    for i in v {
        match parse_int(i.to_owned()) {
            Ok(parsed_integer) => {
                // If parsing is successful, we just add it to total sum
                sum += parsed_integer;
            },
            Err(e) => {
                // If there is an Error, return it immediately
                return Err(e);
            }
        };
    }
    return Ok(sum);
}

fn parse_int(str: String) -> Result<i32, ParseIntError> {
    let x = str.parse::<i32>();
    let value: i32;
    match x{
        Ok(v) => {value = v;},
        Err(e) => return Err(e)
    };
    // Let's assume, for simplicity, some processing on `value` is needed after `.parse`.
    // Otherwise we could have just returned value from `.prase`
    return Ok(value);
}

# output
Successfully parsed vec to 6
Completed
Enter fullscreen mode Exit fullscreen mode

The above example works fine, but it is very verbose to write. For such operations, we can use the handy ? operator. This question mark operator will either consume the value if Ok(T) or it will propagate the error by returning Err(E) to the calling function. Let's rewrite the above program using the ? operator

use std::num::ParseIntError;

fn main()-> Result<(), ParseIntError> {
    let strings_vec: Vec<&str> = vec!("1", "2", "3");

    let parsed_string = parse_vec_and_sum(strings_vec)?; // operator used here
    println!("Successfully parsed vec to {}", parsed_string);
    println!("Completed");
    return Ok(());
}

fn parse_vec_and_sum(v: Vec<&str>) -> Result<i32, ParseIntError> {
    let mut sum: i32 = 0;
    for i in v {
        sum += parse_int(i.to_owned())?; // operator used here
    }
    return Ok(sum);
}

fn parse_int(str: String) -> Result<i32, ParseIntError> {
    let x = str.parse::<i32>()?; // operator used here
    // Let's assume, for simplicity, some processing on `value` is needed after `.parse`.
    // Otherwise we could have just returned value from `.prase` directly
    return Ok(x);
}

# output
Successfully parsed vec to 6
Completed
Enter fullscreen mode Exit fullscreen mode

Now, let's change the vec to something which can cause an error

...
    let strings_vec: Vec<&str> = vec!("john", "2", "3");
...

# output
There was an error parsing the input
Completed
Enter fullscreen mode Exit fullscreen mode

The error ParseIntError is propagated from parse_int -> parse_vec_and_sum -> main. This makes our program even less verbose and a breeze to write.

Please note, the above example is a bit drawn out, it can all be packed into a main function but it is deliberately split into multiple functions to explain error propagation

Closing words

So far we have looked at basic error handling in Rust. We have looked at

  • Result enum which can either be success Ok(T) or failure Err(E)
  • Using .unwrap to quickly handle Result types
  • Use of ? operator to propagate errors through the calling functions

There is a lot more to learn about errors in Rust. Take the above example using the ? operator, we are returning only one error type, what if multiple errors are returned by each function? or What if the error in a Result is dynamic and we don't exactly know its type? How are we to handle such scenarios? Also, there is std::error::Error which is a fundamental trait for representing error values i.e. representing any E in a Result<T, E>. We need to learn a little more about this Error trait.

Don't worry, we can always learn more. But having solid fundamentals will help in understanding more complex topics. I hope this introduction blog has helped some of you to understand errors and error handling in Rust.

Top comments (0)