Three Kinds Of Unwrap

I write a lot of applications in Rust. As a result, I find myself doing a lot of .unwrap()ing, certainly a lot more than if I was writing nice, clean libraries.

One problem I keep running into is trying to remember why I wrote .unwrap() a day or a week later. Did I actually want the application to die on this case, or was I just hastily proving my other code works and wanted to implement error handling later?

I propose that there are three kinds of unwrap, all with different semantics, and all should be treated differently by programmers.

Unwrap as panic!()

The first kind of unwrap is the obvious one; I am unwrapping because if this happens, well, we might as well die.

A good example of this is in some web server code:

let app = Router::new().route("/", get(get_info));
let address_str = format!("{address}:{port}");

// if we've been given an invalid address or port, we can't do anything. Just die!
let addr: SocketAddr = address_str.parse().unwrap();

// if we can't open a tcp socket, we can't do anything. Just die!
let listener = TcpListener::bind(&addr).await.unwrap();

// and if our web server spontaneously dies, we should die too!
axum::serve(listener, app.into_make_service()).await.unwrap();

All of these .unwrap()s are intended to serve the same purpose. If we cannot do this, die.

These unwraps also are all intentional. They are meant to be there, and aren’t a placeholder for error handling. Furthermore, all of these error cases are real, and can happen, we just don’t want to think about them.

Unwrap as unreachable!()

The second kind of panic is less obvious, but occurs especially if you write a lot of static variables.

A great example of this is declaring regexps.

// we are unwrapping here not because we don't care about the error case, but because
// our error case is ABSOLUTELY unreachable!
static HEXADECIMAL_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("^[0-9a-f]*$").unwrap());

This unwrap serves a different purpose to the ones in our web server example. We are unwrapping because this error case cannot happen. Similarly, though, this .unwrap() is intentional and is not a placeholder for error handling.

Unwrap as todo!()

Everyone who has written applications in rust is guilty of using .unwrap() as “ah, i’ll handle errors later, I just want to see if my code actually works on the happy path.”

In fact, anyone who has worked on large applications in rust has probably spent a good deal of their time tracking down these “temporary” unwraps that have been forgotten about!

A decent example of this is in quick-and-dirty rust code.

// ah i'll handle this better later
let age: i32 = user_input.parse().unwrap();

// or... ah this file exists but i'll handle it better later.
let file: Vec<u8> = fs::read("data.txt").unwrap();

This is dirty rust code, but it is common to write this when you’re just trying to prove something works.

This unwrap serves a radically different purpose to the other ones. We are unwrapping because we haven’t implemented error handling yet.

So what?

What I’m pointing out here is that there are three different reasons .unwrap() can be in your code:

But the absolutely critical issue here is that this information is not stored in the code, it’s stored in your head.

Some people write comments around this like // TODO or // cannot happen.

Some use .expect("todo") or .expect("must be valid regex"). I think these are dirty hacks and still don’t accurately preserve the semantics of why we’re unwrapping.

We already have prior art for this sort of “semantic panicking”, in the form of todo!() and unreachable!() macros. Why not have those here?

What do you propose?

I’ve written an RFC which proposes two new methods for Result and Option which are intended to map out these semantics, and prevent confusion w/r/t unwrap.

You can read the proposal here, but the gist of it is this:

// unwrap is still used for the analogous-to-panic!() case
TcpListener::bind(&addr).unwrap();

// we're panicking because error handling is not implemented yet.
// this use case is common in prototype applications.
let int: i32 = input.parse().todo();
let arg2 = std::env::args().nth(2).todo();
let data: Vec<u8> = fs::read("data.txt").todo();

// these error states are unreachable.
// this use case is common in static declarations.
NonZeroU32::new(10).unreachable();
Regex::new("^[a-f]{5}$").unreachable();

It proposes Option::todo, Option::unreachable, Result::todo and Result::unreachable functions.

These serve similar purposes to todo!() and unreachable!() macros, respectively.

With these implemented in the standard library, #[clippy::todo] and other features can point out temporary unwraps that shouldn’t make it to production

If this seems useful to you, or if this is a problem you’ve ran into before, I’d love to see what you think in the RFC! I, personally, would get a lot of value out of these functions.

(RFC)

footnote on naming

The RFC mentions that we could also call these functions unwrap_todo and unwrap_unreachable. I’m a little skeptical of this because unwrap_todo is quite a lot of characters to type, and I’m unsure whether the function will be passed-over because of that, in lazy contexts.

I don’t think that .todo() on its own is particularly confusing as a function; it’s certainly no more confusing than .expect().