Intro to errors in Rust
Tags: rust
Published: 2023-06-24 13:51:37

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

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

# output
10
```

The above conversion from string to an integer executes successfully

```python
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'
```

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.

```python
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
```

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.

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

_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

```rust
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
```

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`.

```rust
pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
    ...
}
```

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

```rust
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
```

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,

```rust
    let _parsed_string: i32 = "10".to_owned().parse::<i32>().unwrap();
```

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,

```rust
    let _parsed_string: i32 = "john".to_owned().parse::<i32>().unwrap();
```

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(`match`ing 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`.

```rust
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
```

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

```rust
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
```

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

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

# output
There was an error parsing the input
Completed
```

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.