Rants, Ideas, Stuff

A blog about coding, coffee, and stuff

Why Rust's error handling is awesome

This post is about the process of transforming something you would write as a one-off script in Python (or any other scripting language) into a library including error handling.

One aspect of Rust I prefer over Python’s way is that error handling is explicit. If try you access a non existant entry in a Python dict you get an KeyError exception. Not having to handle errors is great if you explore an API or write a one-off script. Often these scripts go into production though and fail in the worst possible situations.

Of course you can handle Python’s exceptions, but you have to know where they might occur, or catch them very unspecific.

Here’s an example of getting JSON formatted data from a weather API with Python without error handling:

1
2
3
4
5
6
import requests

if __name__ == '__main__':
    title = requests.get("https://www.metaweather.com/api/location/44418/")
                    .json()['title']
    print title

The Rust implementation without error handling could look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
extern crate reqwest;

extern crate serde;
extern crate serde_json;

fn main() {
    let url = "https://www.metaweather.com/api/location/44418/";
    let location: serde_json::Value = reqwest::get(url)
                                               .unwrap()
                                               .json()
                                               .unwrap();
    let title = location.get("title").unwrap();

    println!("{}", title);
}

In this example we see Rust’s explicit .unwrap()s. While they might feel verbose, this really helps to see where we might want to add error handling.

The first refactoring is to introduce a struct to get type safety and make the parsing more robust. In cases when there is are no title or woeid, or they had different types the parsing in line 18 would fail with an JSON error like ““invalid type: string \“44418\“, expected u32”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
extern crate reqwest;

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

#[derive(Deserialize, Debug)]
struct Location {
    pub title: String,
    pub woeid: u32,
}

fn main() {
    let url = "https://www.metaweather.com/api/location/44418/";
    let location: Location = reqwest::get(url)
                                      .unwrap()
                                      .json()
                                      .unwrap();
    println!("{}", location.title);
}

The next refactoring is extracting a function that returns a Result. The benefit will be more obvious in the next step, but already we gain reusability for our pseudo libarary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
extern crate reqwest;

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

extern crate failure;

use failure::Error;

#[derive(Deserialize, Debug)]
struct Location {
    title: String,
    woeid: u32,
}

fn get_location(url: &str) -> Result<Location, Error> {
    let location: Location = reqwest::get(url)?
                                      .json()?;
    return Ok(location);
}

fn main() {
    let url = "https://www.metaweather.com/api/location/44418/";
    match get_location(url) {
        Ok(location) => println!("{}", location.title),
        Err(e) => eprintln!("{:?}", e),
    }
}

By adding failure::ResultExt we gain the ability to add context to results, so we always know where something went wrong.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
extern crate reqwest;

extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;

extern crate failure;

use failure::{Error, ResultExt};

#[derive(Deserialize, Debug)]
struct Location {
    title: String,
    woeid: u32,
}

fn get_location(url: &str) -> Result<Location, Error> {
    let location: Location = reqwest::get(url)
        .context("error while getting location")?
        .json()
        .context("Could not parse JSON")?;
    return Ok(location);
}

fn main() {
    let url = "https://www.metaweather.com/api/location/44418/";
    match get_location(url) {
        Ok(location) => println!("{}", location.title),
        Err(e) => eprintln!("{:?}", e),
    }
}

Now we can pull our Location and get_location to a lib.rs and reuse it wherever we need it; building statically linked binaries we can distribute and run without the need to setup rvm, virtualenv or ay other environment management tool.

Thanks for reading.