Taking the Unhappy Path with Result, Option, unwrap and ? operator in Rust

codeprototype

Kevin Le

Posted on June 5, 2020

Taking the Unhappy Path with Result, Option, unwrap and ? operator in Rust

As I'm learning Rust, I noticed that these 4 concepts Result, Option, unwrap, ? are often discussed in conjunction with each other. In this post, I'd like to document and share the thought process I have been going through. This thought process has helped me understanding and how to be more idiomatic in Rust.

Option

Although Rust has a concept of null, using null is not a common pattern in Rust. So let's say we have to write a function that, given the name of an mobile OS, the function will return the name of the store. If the string literal iOS is passed as input to the function, it will return App Store. If android, then Play Store. Any other input is considered invalid. In most other languages, we can choose to return null or a string literal invalid or something like that. But doing so is not the Rust way.

One idiomatic way in Rust to implement such function is to return an Option. An Option or to be exact an Option<T> is a generic and can be either Some<T> or None (From here on, I will mostly drop the generic type parameter T so the sentences do not get so cluttered). Rust refers to 'Some' and 'None' as variants (which does not have any equivalent in other languages, so I just don't get so hanged up on trying to define what a variant is). In our example, in the happy paths, the function returns the string literal "App Store" or "Play Store" wrapped in a Some variant. In the unhappy path, the function returns None.

fn find_store(mobile_os: &str) -> Option<&str> {
    match mobile_os {
        "iOS" => Some("App Store"),
        "android" => Some("Play Store"),
        _ => None
    }
}

To consume find_store(), we can call it as follow:

fn main() {
    println!("{}", match find_store("windows") {
        Some(s) => s,
        None => "Not a valid mobile OS"
    });
}

You can try to run the above example in the playground

Result

Result is related to Option in Rust in that Result or to be exact Result<T, E> is a richer version of Option.

Result<T, E> could have one of two outcomes:

  • Ok(T): An element T was found
  • Err(E): An error was found with element E

as opposed to Option<T> previously seen that can contains Some<T> or None. Result has information about the error which is absent in Option.

Let's look at an example of a function that returns a Result. This function is from the serde_json crate which is used to parse JSON. The signature of this function is

pub fn from_str<'a, T>(s: &'a str) -> Result<T, Error> 
where
    T: Deserialize<'a>, 

Assume we want to parse the following String

let json_string = r#"
        {
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        }"#;

to a person object of this struct

#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

the code that parses the json_string to the Person object is as follow:

let p:Person = match serde_json::from_str(json_string) {
    Ok(p) => p,
    Err(e) => ... //we will discuss what goes here next 
};

The happy path is obvious. But let's say there's a typo in the input json_string, match will send the program flow to the Err(e) clause.

When the Err is encountered, there are 2 actions that we can take:

  1. panic! (more about panic) here and here
  2. return the Err

Back to our example, here's the full code if we decide to panic!

Playground

or if we want to return Err

Playground

unwrap

In the above example, let's consider at the case where we decide to panic!. The lines of code above

let p: Person = match serde_json::from_str(data) {
        Ok(p) => p,
        Err(e) => panic!("cannot parse JSON {:?}, e"), //panic
    }

is very verbose. Happy path is handled normally as expected, no discussion there. And when the Err is encountered, the above code panic!. We can replace the above verbose code with

let p:Person = serde_json::from_str(data).unwrap();

unwrap is OK to use if we know for sure that the input json_string will always be parsable to the Person struct, i.e. always happy path or if in case the Err is encountered, the program can't go on and it's unrecoverable. During development, when we're more concerned with getting the general flow of the program, unwrap can also be used as quick prototype.

So unwrap is implicit panic!. Although there's no difference with the more verbose counterpart, the danger is it's implicit, so sometimes it's not really what really want to do.

Regardless, if we need to call panic!, the code is shown as below:

Playground

? operator

As opposed to panic! when the Err is encountered, we can return the Err. Not every Err is unrecoverable, so there's no reason to panic!. The verbose code to return Err is shown again below:

let p: Person = match serde_json::from_str(data) {
        Ok(p) => p,
        Err(e) => return Err(e.into()),
};

? offers us a more concise way to replace the verbose code above:

let p:Person = serde_json::from_str(data)?;

So the entire program is showm here

Playground

Unpacking Option with unwrap and ?

Just as we could use unwrap and ? with Result, we can also use unwrap and ? with Option

If we unwrap an Option that has the underlying value of None, the program panic!

Here's an example of how to unwrap an Option:


Playground

Similarly, we can also unpack an Option with the ? operator. If the outcome is None, the program will terminate whatever function is being executed and return None.

💖 💪 🙅 🚩
codeprototype
Kevin Le

Posted on June 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related