DEV Community

Dimitri Merejkowsky
Dimitri Merejkowsky

Posted on • Originally published at dmerej.info on

Killing unwrap()

Originally published on my blog.

Wait, what? Who do you want to kill?

In Rust, to indicate errors or absence of a value we use types named Result and Option respectively.

If we need the value of an Result or Option, we can write code like this:

let maybe_bar = get_bar();
// bar is now an Option<String>
if maybe_bar.is_some() {
  let bar = bar.unwrap();
  // now bar is a String
} else {
  // handle the absence of bar
}
Enter fullscreen mode Exit fullscreen mode

This works well but there’s a problem: if after some refactoring the if statement is not called, the entire program will crash with: called Option::unwrap on a None value.

This is fine if unwrap() is called in a test, but in production code it’s best to prevent panics altogether.

So that’s the why. Let’s see the how.

Example 1 - Handling None

Let’s go back to our first example: we’ll assume there is a bar::return_opt() function coming from an external crate and returning an Option<Bar>, and that we are calling it in my_func, a function also returning an option:

fn my_func() -> Option<Foo> {
  let opt = bar::return_opt();
  if opt.is_none() {
    return None;
  }
  let value = opt.unwrap();
  ...
  // doing something with `value` here
}
Enter fullscreen mode Exit fullscreen mode

So how do we get rid of the unwrap() here? Simple, with the question mark operator:

fn my_func() -> Option<Foo> {
  let value = bar::return_opt()?;
  // Done: the question mark will cause the function to
  // return None automatically if bar::return_opt() is None
  ...

  // We can use `value` here directly!
}
Enter fullscreen mode Exit fullscreen mode

If my_func() does not return an Option or a Result you cannot use this technique, but a match may be used to keep the “early return”pattern:

fn my_func() {
  let value = match bar::return_opt() {
      None => return,
      Some(v) => v
  };
  ...
}
Enter fullscreen mode Exit fullscreen mode

Note the how the match expression and the let statement are combined. Yay Rust!

Example 2 - Handling Result

Let’s see the bad code first:

fn my_func() -> Result<Foo, MyError> {
  let res = bar::return_res();
  if res.is_err() {
    return Err(MyError::new(res.unwrap_err());
  }
  let value = res.unwrap();
  ...
}
Enter fullscreen mode Exit fullscreen mode

Here the bar::return_res() function returns a Result<BarError, Bar> (whereBarError and Bar are defined in an external crate). The MyError type is in the current crate.

I don’t know about you, but I really hate the 4th line: return Err(MyError::new(res.unwrap_err()); What a mouthful!

Let’s see some ways to rewrite it.

Using From

One solution is to use the question mark operator anyway:

fn my_func() -> Result<Foo, Error> {
  let value = bar::return_res()?;
}
Enter fullscreen mode Exit fullscreen mode

The code won’t compile of course, but the compiler will tell you what to do,and you’ll just need to implement the From trait:

impl From<BarError> for MyError {
    fn from(error: BarError) -> MyError {
        Error::new(&format!("bar error: {}", error))
    }
}
Enter fullscreen mode Exit fullscreen mode

This works fine unless you need to add some context (for instance, you may have an IOError but not the name of the file that caused it).

Using map_err

Here’s map_err in action:

fn my_func() -> Result<Foo, Error> {
  let res = bar::return_res():
  let some_context = ....;
  let value = res.map_err(|e| MyError::new(e, some_context))?;
}
Enter fullscreen mode Exit fullscreen mode

We can still use the question mark operator, the ugly Err(MyError::new(...))is gone, and we can provide some additional context in our custom Error type. Epic win!

Example 3 - Converting to Option

This time we are calling a function that returns an Error and we want a Option.

Again, let’s start with the “bad” version:

fn my_func() -> Result<Foo, MyError> {
  let res = bar::return_opt();
  if res.is_none() {
    retrun Err(MyError::new(....));
  }
  let res = res.unwrap();

  ...
}
Enter fullscreen mode Exit fullscreen mode

The solution is to use ok_or_else, a bit like how we used the unwrap_err before:

fn my_func() -> Result<Foo, MyError> {
  let value = bar::return_opt().ok_or((MyError::new(...))?;
  ...
}
Enter fullscreen mode Exit fullscreen mode

Example 4 - Assertions

Sometime you may want to catch errors that are a consequence of faulty logic within the code.

For instance:

let mystring = format!("{}: {}", spam, eggs);
// ... some code here
let index = mystring.find(':').unwrap();
Enter fullscreen mode Exit fullscreen mode

We’ve built an immutable string with format() and we put a colon in the format string. There’s no way for the string to not contain a colon in the last line, and so we know that find will return something.

I reckon we should still kill the unwrap() here and make the error message clearer with expect():

let index = mystring.find(':').expect("my_string should contain a colon");
Enter fullscreen mode Exit fullscreen mode

In tests and main

Note: I'd like to thank Jeikabu whose comment on dev.to triggered the addition of this section:

Let's take another example. Here is the code under test:

struct Foo { ... };

fn setup_foo() -> Result<Foo, Error> {
    ...
}

fn frob_foo(foo: Foo) -> Result<(), Error> {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Traditionally, you had to write tests for setup_foo() and frob_foo() this way:

#[test]
fn test_foo {
  let foo = setup_foo().unwrap();
  frob_foo(foo).unwrap();
}
Enter fullscreen mode Exit fullscreen mode

But since recent versions of Rust you can write the same test this way[^1]:

#[test]
fn test_foo -> Result<(), MyError> {
  let foo = setup_foo()?;
  frob_foo(foo)
}
Enter fullscreen mode Exit fullscreen mode

Another big win in legibility, don't you agree?

By the way, the same technique can be used with the main() function (the entry point for Rust executables):

// Old version
fn main() {
   let result = setup_foo().unwrap();
   ...
}

// New version:
fn main() -> Result<(), MyError> {
   let foo = setup_foo()?;
   ...
}
Enter fullscreen mode Exit fullscreen mode

Closing thoughts

When using Option or Result types in your own code, take some time to read(and re-read) the documentation. Rust standard library gives you many ways to solve the task at hand, and sometimes you’ll find a function that does exactly what you need, leading to shorter, cleaner and more idiomatic Rust code.

If you come up with better solutions or other examples, please let me know! Until then, happy Rusting :)

Top comments (2)

Collapse
 
jeikabu profile image
jeikabu • Edited

I'm definitely guilty of excessive unwrap()-ing. It's just so easy to do and it feels like an assert().
And good job pointing out the combinators on Result and Option. Just this weekend I realized I had re-invented map() (poorly) and went back and re-factored some code.
One thing I found really helpful is main and tests being able to -> Result<>. Makes it a lot easier to move "correct" code using ? between tests/lib/bin.

Collapse
 
dmerejkowsky profile image
Dimitri Merejkowsky

Did not know about tests being able to return Result<>, thanks for pointing that out.

I'll update the article