Rust #3: Options, Results and Errors (Part 1)
Tags: rust
Published: 2021-07-03 18:16:34

## Options and Results

The `Option` and `Result` types in Rust will be two of the most used types you will have at your disposal when writing your programs. Their concepts are simple but their use can be confusing at times for beginners. It was for me. This blog entry is an attempt to help explain how to use them effectively.

A `Result` can also wrap a Rust error and this blog article will cover how to create those easily too.

Let's look at the basics.

### Option

The `Option` type allows you to have a variable that may or may not contain a value. This is useful for passing optional parameters or as a return value from a function that may or may not succeed.

Its definition is

```rust
enum Option<T> {
    Some(T),
    None,
}
```

So it can either contain a single value via the `Some` variant or no value at all via the `None` variant.

Enums in Rust are implemented with a discriminant value that tells Rust what type of variant is stored and a union of all the data in the variants. So, if you were to implement the same thing in C, it would look something like:

```C
struct Option
{
    int type;
    union 
    {
        struct Some 
        {
            T t;
        };
        struct None {};
    };
};
```

So, the size of the enum is usually the size of the largest variant plus the size of the type value.

But Rust has a neat optimisation. If one variant has no data and the other has a single value that is a non-null pointer (such as references, boxes, function pointers), Rust will optimise the enum type so that its size is the same as the type T. It accomplishes this by representing the no-value variant (e.g. None) as a null pointer. This means something like `Option<&T>` is the same size as `&T`. Effectively, this is like normal C pointers with the extra type safety.

For more information about this check out the documentation of `Option` [here](https://doc.rust-lang.org/std/option/) under the section titled 'Representation'.

Below is an example of how we can use `Option` for a generic function that returns the first item:

```rust
fn first_item<T>(v: &Vec<T>) -> Option<T>
where T: Clone {
    if v.len() > 0 {
        Some(v[0].clone())
    } else {
        None
    }
}
```

The function `first_item` can only return a value if the vector being passed is not empty. This is a good candidate for `Option`. If the vector is empty, we return `None`, otherwise we return a copy of the value via `Some`.

The `None` variant forces the programmer to consider the case where the information required is not forthcoming.

## Result

`Result` is similar to `Option` in that it can either return a value or it doesn't and is usually used as a return value from a function. But instead of returning a `None` value, it returns an error value that hopefully encapsulates the information of why it went wrong.

Its form is:

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

If everything goes well, a function can return an `Ok` variant along with the final result of the function. However, if something fails within the function it can return an `Err` variant along with the error value.

Let's look at an example:

```rust
use std::fs::File;
use std::io::{BufRead, BufReader, Error};

fn first_line(path: &str) -> Result<String, Error> {
    let f = File::open(path);

    match f {
        Ok(f) => {
            let mut buf = BufReader::new(f);
            let mut line = String::new();
            match buf.read_line(&mut line) {
                Ok(_) => Ok(line),
                Err(e) => Err(e),
            }
        }
        Err(e) => Err(e),
    }
}
```

`std::fs::File::open` will return a `Result<std::fs::File, std::io::Error>`. That is, it will either return a file handle if everything goes OK, or it will return an I/O error if it doesn't. We can match on this. If it's an error, we just return it immediately. Otherwise, we try to read the first line of that file via the `std::io::BufReader` type. 

The `read_line` method returns a `Result<String, std::io:Error>` and once again we match on this. If it was an error, we return it immediately. Notice that the error type for both the `open` and `read_line` methods is `std::io::Error`. If they were different, this function wouldn't compile. We will deal with differing error types later.

However, if we were successful, we return the first line as a string via the `Ok` variant.

### The ? operator

Rust introduced an operator `?` that made handling errors less verbose. Basically, it turns code like this:

```rust
let x = function_that_may_fail();
let value = match x {
    Ok(v) => value,
    Err(e) => return Err(e);
}
```

into:

```rust
let value = function_that_may_fail()?;
```

The `?` operator changes the `Result<T,E>` value into one of type `T`. However, if the result was an error, the current function exits immediately with the same 'Err' variant. It unwraps the result if everything went OK, or it causes the function to return with an error if not.

With this in mind, we can simplify the `first_line` demo function above:

```rust
use std::fs::File;
use std::io::{BufRead, BufReader, Error};

fn first_line(path: &str) -> Result<String, Error> {
    let f = File::open(path)?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)?;
    Ok(line)
}
```

I think we can all agree this is a lot easier to read.

### Errors

The error type in a `Result` can be any type, like, for example, a `String`. However, it is recommended to use a type that implements the trait `std::error::Error`. By using this standard trait, users can handle your errors better and even aggregate them.

Traits are interfaces that structures can implement as methods to extend them. I might write a blog article about traits in the future, but if you are not sure what they are, please read this [article](https://blog.rust-lang.org/2015/05/11/traits.html).

Here is the `Error` trait in all its glory:

```rust
trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { None };
    fn backtrace(&self) -> Option<&Backtrace> { None };
}
```

The `backtrace` method is only defined in the nightly version of the compiler, at time of writing, and so only `source` is defined for the stable version. `source` can be implemented to return an earlier error that this current error would be chained to. But if there is no previous error, `None` is returned. Returning `None` is the default implementation of this method.

A type that implements `Error` must also implement `Debug` and `Display` traits.

Errors can be enums too. Below is an example of possible errors that can occur when reading from a file-based database:

```rust
use std::fmt::{Result, Formatter};
use std::fs::File;

#[derive(Debug)]
enum MyError {
    DatabaseNotFound(String),
    CannotOpenDatabase(String),
    CannotReadDatabase(String, File),
}

impl std::error::Error for MyError{}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        match self {
            Self::DatabaseNotFound(ref str) => write!(f, "File `{}` not found", str),
            Self::CannotOpenDatabase(ref String) => write!(f, "Cannot open database: {}", str),
            Self::CannotReadDatabase(ref String, _) => write!(f, "Cannot read database: {}", str),
        }
    }
}
```

First we define the enum with the possible error states and their associative data (e.g. filename of file that was not found). Notice the `derive` macro that implements the `Debug` trait for us. Unfortunately, we cannot do that for `Display` traits.

Secondly, we implement the `Error` trait for compatibility with other error systems. Since we're not chaining errors, the default implementation will do.

Finally, we implement the `Display` trait, which is a requirement of the `Error`trait.

This is a lot to write for an error type, but fortunately there are some popular crates that allow us to write and use errors more easily.

## Error Crates

As just shown, implementing an error type to be passed with a `Result`'s `Err` variant can be tedious to write. Some consider the `Error` trait lacking in functionality too. Various crates have been written to combat the boilerplate and to increase the usefulness of the types of error values you can generate.

I will explore a few of them in this article that I have found to be the most popular.

Let us imagine we want to implement this function:

```rust
fn first_line(path: &str) -> Result<String, FirstLineError> { ... }
```

We will investigate how we implement `FirstLineError` in each of these crates. The basic foundation of the error will be this enum:

```rust
enum FirstLineError {
    CannotOpenFile { name: String },
    NoLines,
}
```

### `failure` crate

`failure` provides 2 major concepts: the `Fail` trait and an `Error` type.

The `Fail` trait is a new custom error type specifically to hold better error information.  This trait is used by libraries to define new error types.

The `Error` trait is a wrapper around the `Fail` types that can be used to compose higher-level errors. For example, a file open error can be linked to a database open error. The user would deal with the database open error, and could dig down further and obtain the original file error if they wanted.

Generally, crate writers would use `Fail` and crate users would interact with the `Error` types.

`Failure` also supports backtraces if the crate feature `backtrace` is enabled and the `RUST_BACKTRACE` environment variable is set to `1`.

This is how we would create the `FirstLineError` error type using this crate:

```rust
use std::fs::File;
use std::io::{BufRead, BufReader};

use failure::Fail;

#[derive(Fail, Debug)]
enum FirstLineError {
    #[fail(display = "Cannot open file `{}`", name)]
    CannotOpenFile { name: String },
    #[fail(display = "No lines found")]
    NoLines,
}

fn first_line(path: &str) -> Result<String, FirstLineError> {
    let f = File::open(path).map_err(|_| FirstLineError::CannotOpenFile {
        name: String::from(path),
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .map_err(|_| FirstLineError::NoLines)?;
    Ok(line)
}
```

The derive macro implements the `Fail` and `Display` traits automatically for us.  It uses the `fail` attributes to help it construct those traits.

But we do have another problem. The `File::open` and `BufRead::read_line` methods return a result based on the `std::io::Error` type and not the `FirstLineError` type that we require. We use the `Result`'s `map_err` method to convert one error type to another.

I will cover `map_err` and other methods for `Option` and `Result` in my next blog article, but for now I will describe this one. If the result is an error, `map_err` will call the closure given with the error value allowing us an opportunity to replace it with a different error value.

So, recall that `File::open` returns a `Result<(), std::io::Error` value. By calling `map_err` we now return a `Result<(), FirstLineError>` value. This is because the closure given returns a `FirstLineError` value and through type inference, we get the new result type. If the result is an error, that closure will provide the value to associate with the `Err` variant.

But the value returned from `File::open` is still a `Result` type so we use the `?` operator to exit immediately if an error occurs.

Now we can do things like:

```rust
match first_line("foo.txt") {
    Ok(line) => println!("First line: {}", line),
    Err(e) => println!("Error occurred: {}", e),
}
```

`Failure` can even allow you to create errors on the fly that are compatible with `failure::Error`.  For example,

```rust
use failure::{ensure, Error};

fn check_even(num: i32) -> Result<(), Error> {
    ensure!(num % 2 == 0, "Number is not even");
    Ok(())
}

fn main() {
    match check_even(41) {
        Ok(()) => println!("It's even!"),
        Err(e) => println!("{}", e),
    }
}
```

This program will output `Number is not even` as expected via the `Display` trait of the error.

There are other ways to create errors on the fly too with `failure`.  `format_err!` will create a string based error:

```rust
let err = format_err!("File not found: {}", file_name);
```

And finally, there's a macro that combines `format_err!` with a return:

```rust
bail!("File not found: {}", file_name);
```

### `snafu` crate

This is similar to `failure` but solves the issue where the actual error that occurred is not the error we want to report.

If you recall above, we use `map_err` to convert the `std::io::Error` into one of our `FirstLineError` variants. `snafu` makes this easier by providing a `context` method that allows the programmer to pass in the actual error they wish to report.

Let's redefine our error type and function using `snafu`:

```rust
use std::fs::File;
use std::io::{BufRead, BufReader};

use snafu::{Snafu, ResultExt};

#[derive(Snafu, Debug)]
enum FirstLineError {
    #[snafu(display("Cannot open file {} because: {}", name, source))]
    CannotOpenFile {
        name: String,
        source: std::io::Error,
    },
    #[snafu(display("No lines found because: {}", source))]
    NoLines { source: std::io::Error },
}

fn first_line(path: &str) -> Result<String, FirstLineError> {
    let f = File::open(path).context(CannotOpenFile {
        name: String::from(path),
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .context(NoLines)?;
    Ok(line)
}
```

To use `context()`, there needs to be a `source` field in the variant. Notice that the enum type `FirstLineError` is not included. We wrote `CannotOpenFile`, not `FirstLineError::CannotOpenFile`. And the source field is automatically set! There's some black magic going on there!

If you don't want to use the name `source` for your underlying cause, you can rename it by marking the field you do want to be the source with `#[snafu(source)]`. Also, if there is a field called `source` that you don't want to be treated as `snafu`'s source field, mark it with `#[snafu(source(false))]`.

Similarly, `snafu` supports the `backtrace` field too to store a backtrace at point of error. `#[snafu(backtrace)]` et al. controls those fields like the source.

On top of this, you have the `ensure!` macro that functions like `failure`'s.

### `anyhow` crate

This crate provides dynamic error support via its `anyhow::Result<T>`. This type can receive any error.  It can create an ad-hoc error from a string using `anyhow!`:

```rust
let err = anyhow!("File not found: {}", file_name);
```

It also defines `bail!` and `ensure!` like other crates. `anyhow` results can extend the errors using a `context()` method:

```rust
let err = anyhow!("File not found: {}", file_name)
    .context("Tried to load the configuration file");
```

Here's the `first_line` method implemented using `anyhow`:

```rust
use anyhow::Result;

#[derive(Debug)]
enum FirstLineError {
    CannotOpenFile {
        name: String,
        source: std::io::Error,
    },
    NoLines {
        source: std::io::Error,
    },
}

impl std::error::Error for FirstLineError {}

impl std::fmt::Display for FirstLineError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            FirstLineError::CannotOpenFile { name, source } => {
                write!(f, "Cannot open file `{}` because: {}", name, source)
            }
            FirstLineError::NoLines { source } => {
                write!(f, "Cannot find line in file because: {}", source)
            }
        }
    }
}

fn first_line(path: &str) -> Result<String> {
    let f = File::open(path).map_err(|e| FirstLineError::CannotOpenFile {
        name: String::from(path),
        source: e,
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .map_err(|e| FirstLineError::NoLines { source: e })?;
    Ok(line)
}
```

`anyhow` doesn't define the `Display` trait for us so we have to do that ourselves. Also `map_err` has to come back if we want to convert error values from one domain to another. But, this time we use `Result<String>` and we don't need to define which error is returned.

### `thiserror` crate

This crate makes it easier to define the error type, and can be used in conjunction with `anyhow`. It uses `#[derive(thiserror::Error)]` to generate all the `Display` and `std::error::Error` boilerplate like other crates do.

But `thiserror` makes it easier to chain lower-level errors using the `#[from]` attribute.  For example:

```rust
#[derive(Error, Debug)]
enum MyError {
    #[error("Everything blew up!")]
    BlewUp,
    
    #[error(transparent)]
    IoError(#[from] std::io::Error)
}
```

This will allow auto-casting from `std::io::Error` to `MyError::IoError`.

Let's look at our demo with `anyhow` for results, and `thiserror` for errors:

```rust
use std::fs::File;
use std::io::{BufRead, BufReader};

use anyhow::Result;
use thiserror::Error;

#[derive(Debug, Error)]
enum FirstLineError {
    #[error("Cannot open file `{name}` because: {source}")]
    CannotOpenFile {
        name: String,
        source: std::io::Error,
    },
    #[error("Cannot find line in file because: {source}")]
    NoLines {
        source: std::io::Error,
    },
}

fn first_line(path: &str) -> Result<String> {
    let f = File::open(path).map_err(|e| FirstLineError::CannotOpenFile {
        name: String::from(path),
        source: e,
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .map_err(|e| FirstLineError::NoLines { source: e })?;
    Ok(line)
}
```

Notice the neat embedded field names in the strings on the `#[error(...)]` lines.

## Main function

A quick note on the main function. Rust Edition 2018 as added a feature that allows `main` to return a `Result`. If main returns an `Err` variant, it will return an error code other than 0 to the operating system (signifying a fail condition), and output the error using the `Debug` trait.

If you wanted the `Display` trait, then you have to put your main function in another, then have your new `main` call it and `println!` the result:

```rust
fn main() -> i32 {
    if let Err(e) = run() {
        println!("{}", e);
        return 1;
    }
    
    return 0;
}

fn run() -> Result<(), Error> { ... }
```

If the `Debug` trait is good enough for printing out your error, you can use:

```rust
fn main() -> Result<(), Error> { ...  }
```

How does the program know what error code to return?  It uses the new `Termination` trait:

```rust
trait Termination {
    fn report(self) -> i32;
}
```

The compiler will call `report()` on the type you return from `main`.

## And there's much more...

But not this week...

I wanted to talk about the methods on `Option` and `Result`, like `map_err` but this article is already too long. I will cover them next time.