Category: Rust


Result<Rust>

Rust is a newish systems programming language from Mozilla. I have lately been putting a little bit more systematic effort into learning it. Here I wanted to talk about one of the basics of Rust that I really like: how it handles errors.

Rust doesn't use exceptions to handle errors like JavaScript or Python would do. Instead the error is part of the normal return type of a function. The advantage is that you don't end up with a separate, largely invisible, code path through your code for errors. Instead returning errors is just a normal early return from the function. This makes error handling no longer exceptional, but an ordinary part of the code, as it should be.

This might be sound similar to what Go is doing. In Go the error is also part of the return type of a function. Go does this by returning two separate values from a function that can fail (or a tuple of two values). For example, the signature for the function to open a file in Go is:

func Open(name string) (file *File, err error)

There are two return values: a pointer to a file and an error. One or the other of these values will be nil. If everything went fine then err is nil, but if something went wrong the file is nil and the err contains information about what went wrong.

There are two problems with the Go way of doing this. Generally, only one or the other returned value is a valid value, either file or err. But this fact isn't encoded in the return type. This also leads to the second problem: you can sort of forget to check if err is nil and just go about trying to use file. But if something went wrong attempting to use file will fail, because its nil.

Rust solves both problems in Go by using a Result type. The Result type has two variants: an Ok variant containing the data if everything went ok, and the Err variant containing information about the error in the case something went wrong. The open file method signature in Rust looks like this (somewhat simplified):

fn open(path: &str) -> Result<File, io::Error>

Here the return type is a Result which either contains a File if everything went ok, or an io::Error if something went wrong. io::Error is a struct containing information about what went wrong in an I/O operation. Here the first problem of Go is immediately solved because the Result type can only be Ok xor Err, not both, so the exclusivity is encoded in the type.

The other Go problem is solved by the fact that you can't just grab the Result and try to treat it like a File, you have to extract it first. You might do it like this:

use std::fs::File;

fn main() {
  let file_result = File::open("foo.txt");
  match file_result {
    Ok(file) => {
      // Dome something with the file.
    },
    Err(error) => {
      // Complain to the user that the file couldn't be opened.
    }
  }
}

The match construct allows you to branch your code on the different variants of Result. You also can't just skip the Err branch. The compiler will refuse to compile your code if you don't handle both variants.

The one downside of Rust's error handling that might jump out at you is that it can quickly get very verbose. To help deal with this Rust offers a good number of tools to work with errors.

One is the expect method on Result. If the Result is Ok the method evaluates to the value inside Ok, and if the Result is Err it prints out a message and quits the application. Using it might look something like this:

use std::fs::File;

fn main() {
  let file_result = File::open("foo.txt");
  let file = file_result.expect("Couldn't open file foo.txt")
  // Do something with the file.
}

This makes sense for simple pieces of code where just quitting if something goes wrong is a sensible thing to do, or for the kind of errors that shouldn't happen unless the application is broken somehow.

Another tool in the Rust toolbox is the question mark operator. This one you can only be used inside a function that returns an error. What the question mark operator does is that if the Result is Err it does an early return from the enclosing function, return the error. If the Result is Ok it evaluates to the value contained inside Ok. This might look like this:

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

fn do_foo() -> Result<(), io::Error> {
  let file= File::open("foo.txt")?;
  // Do something with the file.
}

fn main() {
  let result = do_foo();
  match result {
    Err(error) => {
      // Complain to the user that the file couldn't be opened.
    },
    Ok(()) => {}
  }
}

The empty parens, (), is Rust for the void type, for when the function returns nothing in the non-erroneous case. In the match statement I do nothing in the case do_foo returned Ok. The question mark operator allows bubbling up the error case, similar to how expectations bubble up until you catch them, except in Rust you still have to do so explicitly.

I'm sure I will encounter challenges with the way Rust does error handling once I get deeper into it but thus far it really seems to like a substantial improvement over the types of error handling I'm used to from other languages.