Error Handling in Rust

Free Linux Book

Get FREE domain for 1st year and build your brand new site

Let's tackle 2 topics today. One of them is about dealing with possible errors and/or mistakes we might encounter in our developing journey in Rust. The other is about learning how to write code with these in mind.

Table of contents:

  1. Error Handling
  2. To panic! or not to panic!
  3. Result<T,E>
  4. Unwrap and Expect
  5. Error Propagating

Let us get started with Error Handling in Rust.

Error handling

Errors will happen. There's no way around it. Even the best programmers out there make mistakes. The more you make and fix, the less of them you will make. But that only tends to 0. It never is 0. Anyway, how do we deal with errors in Rust?

Rust splits errors into 2 categories. Recoverable errors, and unrecoverable errors. Recoverable errors are the ones that can be quickly/easily fixed by the user, and the operation can be tried again. Unrecoverable errors are more usually symptoms of a bug in our code, like trying to read outside an array's bounds.

Rust does not have exceptions like other languages. It instead uses the return type Result<T, E> for recoverable errors, and the panic! macro for unrecoverable errors. Let's start with the panic macro and unrecoverable errors as these are the more troublesome ones.

To panic! or not to panic!

Errors will happen, as I've said, no matter what. If the error is of the unrecoverable nature, the panic! macro is executed. The program prints an error message to the screen then unwinds( It goes up the stack and clears up all the memory it's allocated, freeing it for the OS to reassign). Once that's done, it quits.

How is panic! used? Simple!

fn main() {
    println!("I am going to panic!");
    panic!("To panic! Or not to panic! That is the question!");
}

Error-1

As you can see, the program halts immediately and it even shows us exactly where in our code it panicked ([...] src/main.rs:3:5, line 3, 5th character in our main.rs file).

That's all fine and dandy, you might say, but if I have to execute the panic! macro, it won't catch errors I don't expect.. Well Rust automatically calls the panic! macro when it encounters an unrecoverable error! Let's try going out of bounds in a vector, like I mentioned in the intro to errors.

fn main() {
    let simple_vec = vec![0, 1, 2];
    println!("{}", simple_vec[10]);
}

Error-2

As soon as we attempt this, Rust stops the program and refuses to continue. This kind of error is important and cannot be overlooked. (Other programming languages might let you continue despite this, handling the responsibility to you as programmer to notice that the program is behaving strangely)

If you need more information as to what happened, you can set, as it says, the RUST_BACKTRACE environment variable to 1. It will spew out a lot of text, the key is finding the code you wrote and go from there. I will not be going over how to do this, you can do a quick google search as to how to do it in your system :D Moving on..

Result<T, E>

Luckily the errors of the unrecoverable type are not the most often encountered error type. Most errors are simple enough to be fixed without halting the program altogether. The easiest and clearest example of this type of error is, for example, trying to open a file that doesn't exist, or is not found. You could either find the file, or create a new one. No need to panic over that! Let's try this, see what happens and how we can deal with it.

use std::fs::File;

fn main() {
    let f = File::open("test.txt");
}

The open function in File returns a Result<T, E>. How do we know? Well, there are several ways to find out. One is to go to the API documentation and look the function up. Another (way too cumbersome) way is to give the variable f another type, and try to compile (Which will fail because we're saying F is of type A, when open returns something of type B). Or, depending on your text editor of choice, you could simply hover over the function.

Error-3

Error-4

But what does it mean it returns Result<T, E> ? What is it?

Result is..

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

If all goes well, it returns T. If something goes bad, it returns the error E. In the case of our open, we can see it's a Result<File>. So if all goes well, it returns a File, otherwise, Error! In both cases, that only sets the variable. So it will not throw an error on it's own. We have to match our variable to the two possible result types.

use std::fs::File;

fn main() {
    let f = File::open("test.txt");

    match f {
        Ok(file) => file,
        Err(error) => panic!("Opening file failed: {:?}", error),
    };
}

With this match we "Unwrap" our Result. If it's okay, we give the file to the variable f. If it contains an error, we panic! and display said error.

Error-5

( My OS is in spanish so, since this error comes from the OS, it's written in spanish. It essentially means the file cannot be found)

We might want to be a bit more specific though. And we can!

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("test.txt");

    match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file {:?}", other_error)
            }
        },
    };
}

The book explains it best so, let me quote it..

The type of the value that File::open returns inside the Err variant is io::Error, which is a struct provided by the standard library. This struct has a method kind that we can call to get an io::ErrorKind value. The enum io::ErrorKind is provided by the standard library and has variants representing the different kinds of errors that might result from an io operation.

It is becoming quite.. cumbersome though.. With how many kinds of errors we could have..

There are shortcuts, and a better way of writing all those match blocks involving closures.. I will let you figure the closures out since I've already explained them in a previous article! Shortcuts though.. There's 2. Unwrap, and Expect.

Unwrap and Expect

The Result type has 2 functions that help keep things smaller, although it's not always advisable to use them. Let's start with Unwrap.

Unwrap simply returns the value if it encounter the Ok() variant, and panics if it encounters the Err() variant. This would shorten our code down to... (I'll leave the extra code commented so you can see the difference)

use std::fs::File;

fn main() {
    let f = File::open("test.txt").unwrap();

    // match f {
    //     Ok(file) => file,
    //     Err(error) => match error.kind() {
    //         ErrorKind::NotFound => match File::create("hello.txt") {
    //             Ok(fc) => fc,
    //             Err(e) => panic!("Problem creating the file: {:?}", e),
    //         },
    //         other_error => {
    //             panic!("Problem opening the file {:?}", other_error)
    //         }
    //     },
    // };
}

Error-6

Unwrap gives us the default panic! message, as we've seen above.

Expect works very similarly to unwrap, but instead lets us add an extra message to display once it panics. I'll highlight it in the result print.

use std::fs::File;

fn main() {
    let f = File::open("test.txt").expect("Failed to open test.txt");
}

Error-7

Error Propagating

This is a method that allows you to, instead of handling possible errors inside the current function you're writing, to pass the error up to the caller of this function and handle the error there. Let's go back real quick to our previous example and tweak it a bit.

use std::fs::File;
use std::io::{self, Read};

// Main snipped!

pub fn read_file() -> Result<String, io::Error> {
    let f = File::open("test.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(error) => panic!("Opening file failed: {:?}", error),
    };

    let mut read_string = String::new();
    match f.read_to_string(&mut read_string) {
        Ok(_) => Ok(read_string),
        Err(e) => Err(e),
    }
}

As you can see, instead of returning a String, and dealing with the errors in here, we can return a Result, and let whoever is calling this function deal with the errors. Now, this is really long and verbose, there is a shortcut for this, the ? operator.

pub fn read_file() -> Result<String, io::Error> {
    let mut f = File::open("test.txt")?;
    let mut read_string = String::new();
    f.read_to_string(&mut read_string)?;
    Ok(read_string)
}

The ? operator when placed after a Result behaves the exact same way as a match -> Ok/Err statement. If it's Ok, it returns the Ok option. If it's an err, it returns the whole function with the value of that Err, as if we had placed a Return statement. Neat and handy. Short and concise. The ? operator can only be used on functions that return Result, Option or any type that implements std::ops::Try.

The main function in rust has a few valid return types. One of which is (), which is what we've been using so far. Or Result<T,E> (So you can indeed use ? inside Main, if you change the return type!)

In this article we've had a birds-eye look of Error handling. For more information you can check the book's chapter, which I've left linked below!

References

Error handling chapter in The Rust Book