Taking the Unhappy Path with Result, Option, unwrap and ? operator in Rust
Kevin Le
Posted on June 5, 2020
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 elementT
was found -
Err(E)
: An error was found with elementE
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:
Back to our example, here's the full code if we decide to panic!
or if we want to return Err
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:
?
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
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
.
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
November 29, 2024