To unwrap() or not to unwrap(), that is the question:

So I've finally given in and started to learn Rust last month. It's a really cool programming language, with some interesting differences to what I've used before. (JavaScript and Python, mostly)

There are some really pawesome guides out there, "The Rust programming language" is definitely a must-read in my opinion, and Rustlings is nyamazing for anyone who likes to learn by actively working through interactive problems.

After reading through a lot of those big thorough guides by experienced Rust developers, I've started working on my first actual Project. I approached the development of this project by just trying to get small parts of it working in any way I can manage, and then build upon this. In that process, I learned a lot of small subtilties that guides like the ones named above just can't really cover. This post is for sharing those things, those cool little tips to make your first Rust project just a little cleaner and more Rust-y. Originally I wanted to make this about a lot of different topics, but then I've realized that my notes already contain so many things about just one part of Rust: The Enums Option and Result. So this post will be about those, and hopefully will mark the start of a series on this blog.

While reading through this, you might think that the things I'm mentioning are obvious. That's okay, and that's the point. Nothing is ever completely obvious to everyone, and this is for those like me, who often don't immediately recognize the "obvious". And, to be honest, I am writing this just as much for myself, writing all of that stuff down to aid me in my own ongoing learning process.

So, let's start!

Firstly, a very quick introduction. Option and Result are part of the Rust standard library. Quoting the official documentation is probably the easiest way to summarize their purpose:

Type Option represents an optional value: every Option is either Some and contains a value, or None, and does not. Option types are very common in Rust code, as they have a number of uses:

  • Initial values
  • Return values for functions that are not defined over their entire input range (partial functions)
  • Return value for otherwise reporting simple errors, where None is returned on error
  • Optional struct fields
  • Struct fields that can be loaned or “taken”
  • Optional function arguments
  • Nullable pointers
  • Swapping things out of difficult situations

and

Result<T, E> is the type used for returning and propagating errors. It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value.

At first, it seems so easy to just add a quick .unwrap() after every Option or Result, but this comes with the disadvantage of your code panicking if it fails to unwrap. Sometimes, this can be useful during development, to discover potential error cases you might not have thought about, but is usually not what you want to happen.

So, what can you do instead?

First of all, don't use unwrap() unless you are completely sure that the value will never panic. Sometimes that is the case, because an earlier part of your code already made sure that it is Ok or Some.

In some cases, you actually want the program to panic. But even then, there is a slightly better way. You can use expect("foo") to add a message to the panic, so the user actually knows what went wrong. That message should be worded in a specific way, basically telling the user what you expected to happen.

fn main() {
    let x = Some("Hello, World");
    // There is no actual "None" type/value in Rust,
    // this "None" is more specifically Option::None
    let y: Option<String> = None;

    let z = x.unwrap(); // We explicitly set this to Some, so we can safely unwrap it
    let rip = y.expect("expected y to not be None");

    println!("{}, {} will never print because it panics above", z, rip);
}

There are also the non-panicking siblings of unwrap(), like unwrap_or(), unwrap_or_else() and unwrap_or_default().

fn main() {
    let a: Option<String> = None;
    let b: Option<i32> = None;
    let c: Option<bool> = None;

    // unwrap_or() lets you supply a specific value to use if the Option is None
    let x = a.unwrap_or("Hello there".to_owned());

    // unwrap_or_default() uses the types default value if the Option is None
    let y = b.unwrap_or_default();

    // unwrap_or_else() lets you specify a closure to run if the Option is None
    let z = c.unwrap_or_else(|| if 1 + 1 == 2 { true} else { false });

    assert_eq!(x, "Hello there".to_owned());
    assert_eq!(y, 0);
    assert_eq!(z, true);
}

And then there is this really cool question-mark operator, which comes in very handy once you go multiple functions deep and keep having to work with more and more Results and Options. The way it works is that, if you have a None or an Error, it passes up the handling of this one level higher, by returning out of the function early with a None or Error value itself.

Of course, since return types of functions have to be known at compile time, the question-mark operator only works inside functions that already return Result or Option.

fn main() {
    let x = 5;
    let y = 10;
    let z = 20;

    match do_something(x, y, z) {
        Some(result) => println!("Happy noises, {}", result),
        None => println!("Sad noises"),
    }
}

fn do_something(x: i32, y: i32, z: i32) -> Option<i32> {
    let first_result = do_something_more(x, y)?;
    let second_result = do_something_more(first_result, z)?;

    Some(second_result)
}

fn do_something_more(x: i32, y: i32) -> Option<i32> {
    Some(x + y)
}

The advantage of this is that you only have to handle your None case exactly once. You don't have to add pattern matching, or conditionals, or unwrap()s all over the place, just a cute little question mark that delegates the handling to some logic higher up.

"But sammy!" you say, "the compiler keeps shouting at me when I use the question mark on Options when my function returns Result `<-._.->´"

Don't worry my frien! Even this has been considered!

First of all, why does the compiler get upset? It's because the question-mark operator returns the same type that it's used on, and Result and Option are different types. Because of that, I thought I'd have to manually handle None cases in all of my Result-returning functions. Until one day, I was reading through some documentation (I know, I know, I'm a nerd who reads through documentation for fun and not just to find specific things) and discovered Option::Ok_or().

Transforms the Option<T> into a Result<T, E>, mapping Some(v) to Ok(v) and None to Err(err).

This was a life-changer to me, and it was just hiding right there in plain sight. Now I can easily turn a None where there shouldn't be a None into an Error to pass up with my pawesome question-mark operator!

fn main() -> Result<(), String> {
    let x = function_that_returns_option().ok_or("error message".to_owned())?;
    // Instead of:
    // let x = function_that_returns_option().unwrap();
    // or any of the other ways to handle None

    assert_eq!(x, ());
    Ok(x)
}

fn function_that_returns_option() -> Option<()> {
    return Some(());
}

The last thing I want to mention is both an example specific to Options, and a more general tip about how I discovered this one. There is this wonderful friend for all Rust developers, called Clippy. No, not the Paperclip from Microsoft Word, but A collection of lints to catch common mistakes and improve your Rust code. Clippy is automatically installed when you install Rust via rustup, and it runs a whole lot of checks against your code to tell you what you can improve.

In my case, I had the following piece of code:

let insert = (
            tracks::title.eq(match tag.title() {
                Some(title) => Some(title.to_string()),
                None => None,
            }),
            tracks::track_number.eq(match tag.track() {
                Some(track) => Some(track as i32),
                None => None,
            }),
            tracks::disc_number.eq(match tag.disk() {
                Some(track) => Some(track as i32),
                None => None,
            }),
            tracks::path.eq(match path.to_str() {
                None => return Err(Error::msg("Could not get path")),
                Some(path) => path.to_string(),
            }),
            tracks::year.eq(match tag.year() {
                Some(year) => Some(year as i32),
                None => None,
            }),
            tracks::album_id.eq(match album {
                Some(album) => Some(album.id),
                None => None,
            }),
        );

This code builds an insert statement for the database holding my music metadata, getting the values from the tags of a file. The tag fields are all Options, since the tags might be empty. The databse entries are also all Options, (at least on the Rust side, on the database they are just values marked as possibly being Null). So my intuitive idea to build this was to just go through all the entries, match the tag, put in Some(value) if there is a value, and Noneif there is none.

It works, it's not wrong, but there is a cleaner and more readable way to do this. And clippy told me right away, I ran it from my IDE, and it told me:

Manual implementation of Option::map

Huh, okay. Let's check the documentation

Maps an Option<T> to Option<U> by applying a function to a contained value.

So basically exactly what I did with those match statements! My IDE even had a button to just easily fix this automatically with one click:

let insert = (
            tracks::title.eq(tag.title().map(|title| title.to_string())),
            tracks::track_number.eq(tag.track().map(|track| track as i32)),
            tracks::disc_number.eq(tag.disk().map(|track| track as i32)),
            tracks::path.eq(match path.to_str() {
                None => return Err(Error::msg("Could not get path")),
                Some(path) => path.to_string(),
            }),
            tracks::year.eq(tag.year().map(|year| year as i32)),
            tracks::album_id.eq(album.map(|album| album.id)),
        );

Great, that looks a lot cleaner immediately! Note how one of the lines was not changed, that's because that one sets a DB value which is NOT NULL, thus if the original Option is a None it means something went wrong, and we should abort this insert and return with an Error.

And with that, we're done with my first blogpost about Rust, with hopefully many more to come! As I said, I am still learning, and writing this is part of my learning process. That being said, if you find this interesting, learned something from it, etc., feel free to leave me some feedback! I'd love to hear what you think! And if I made mistakes, please also tell me. I'm always happy to learn more and to fix those mistakes so others can learn from them too.

Thank you so much for reading 💜