💾 Archived View for dmerej.info › blog › 0093-killing-unwrap.gmi captured on 2021-12-04 at 18:04:22. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-11-30)
-=-=-=-=-=-=-
2019, Jan 30 - Dimitri Merejkowsky License: CC By 4.0
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 }
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.
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 }
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! }
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() -> SomeType { let value = match bar::return_opt() { None => return SomeType::default() Some(v) => v }; // do something with v }
Note the how the `match` expression and the `let` statement are combined. Yay Rust!
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(); ... }
Here the `bar::return_res()` function returns a `Result<BarError, Bar>` (where `BarError` 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.
One solution is to use the question mark operator anyway:
fn my_func() -> Result<Foo, Error> { let value = bar::return_res()?; }
The code won't compile of course, but the compiler will tell you what to do[1], and you'll just need to implement the `From` trait:
1: Letting the compiler tell you what to do - an example using Rust
impl From<BarError> for MyError { fn from(error: BarError) -> MyError { Error::new(&format!("bar error: {}", error)) } }
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).
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))?; }
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!
This time we are calling a function that returns an `Option` and we want a `Result`.
Again, let's start with the "bad" version:
fn my_func() -> Result<Foo, MyError> { let res = bar::return_opt(); if res.is_none() { return Err(MyError::new(....)); } let res = res.unwrap(); ... }
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_else(|| (MyError::new(...))?; ... }
This time we want to discard the error and simply return None.
Bad example (even with the pattern matching):
fn my_func() -> Option<SomeType> { let result = bar::return_res(); let v = match (result) { Err(_) => return None, Ok(v) => v }; Some(v) }
Better example:
fn my_func() -> Option<SomeType> { let v = bar::return_res().ok()?; Some(v) }
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();
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");
2: https://dev.to/jeikabu/comment/8kb4
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> { ... }
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(); }
But since recent versions of Rust you can write the same test this way:
#[test] fn test_foo -> Result<(), MyError> { let foo = setup_foo()?; frob_foo(foo) }
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()?; ... }
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 :)
----