Rust is a reliable language that prioritizes correctness. However, during development, it can be a burden to account for every single error path immediately.

This article presents a few techniques that defer handling correctness to that you can stay longer in the happy path, as well as a library I developed to make this process more convenient.


🌱 100% of this article has been handcrafted without using generative AI tooling, if somewhat hastily. Take my typoes and broken sentences as proof of that.

What's worse for a Rust developer working on some elaborate logic than having rustc suddenly getting in the way? Be it because you didn't return a Result yet, or handle this error case, or implement that edge case, causes for compilation errors are numerous and can distract you from what you're attempting to achieve.

When this happens you're usually presented with two options:

  1. Go with the distraction and immediately implement perfect error handling. Typically you'll have to refactor it a number of times before the PR is ready, making the whole process much more heavy than what is necessary.
  2. Slap some manner of TODO later label on the error, downgrading it to a warning, and move on with your work for now.

This article covers some Rust techniques to achieve the latter option, downgrading an immediate error to a warning to be handled later.

Why do you want warnings?

Warnings occupy the perfect spot in the possible gamut of compilation interactions:

  • Errors block other compilation steps (lifetimes, etc), and testing, impeding the common workflow:

    1. Write WIP code
    2. Test it
    3. Turn WIP code to production code

    Any error prevents going from (1) to (2)

  • Completely suppressing any compilation message, on the other hand, runs the risk of forgetting about the issues and letting them sweep into production 😬

  • Warnings don't block development, but need to be cleaned before merging the PR.

    At Meilisearch, we have a CI that runs the compiler with the -D warning flag, that will turn all warnings into errors at CI time, ensuring we don't ship WIP code accidentally.

Why don't you just read the f**king code???

Of course, I do. Reading code (yours, and the one from your colleagues) is an excellent barrier against bugs, and code reviews are a requirement to effectively share knowledge among your team.

All the techniques discussed in the article come as an additional manner of creating incentives for correctness and stronger guardrails when producing code at a professional scale. They increase quality but don't replace the basic good practices.

In my experience as a software engineer, anything that relies on humans never a mistake is deemed to fail eventually.

Without further ado, here are some techniques you can use in vanilla Rust to implement error deferral strategies.

Techniques with standard Rust

unwrap everywhere

While it is important that Rust gives us the structure to correctly address errors, it doesn't mean we need to address them immediately.

A common pattern for delaying the error handling is to simply unwrap your Result or Option, to get the value inside at the price of a panic on the error path.

Then when the rest of the code is ready, you can go back to your changes and selectively remove the unwrap that were added as a temporary measure, replacing them with proper error handling.

fn does_not_return_result_yet(some_params: SomeType) -> SomeReturnType {
  let converted = some_params.try_into().unwrap() // <- fallible conversion, ignore for now
  converted.try_foo().unwrap() // <- fallible operation, ignore for now
}

clone everywhere

Ownership is a similar issue, albeit slightly different and much less common than error handling. Properly maintaining ownership sometimes mandates design modifications, lest lifetime issues appear. In this case, a frequent mitigation option is to clone a value instead of moving it around.

Delaying the fixes for this category of issues is a bit more delicate, as the fix can entail changing the architecture of the code in non-trivial ways. Still, with experience, it is possible to identify early cases where you'll be able to remove the clones later by passing the appropriate variables higher in the stack.

the todo! macro

The Rust standard library provides a todo!() macro, whose purpose is described as:

Indicates unfinished code.

This can be useful if you are prototyping and just want a placeholder to let your code pass type analysis.

This is indeed extremely useful when shaping out an interface and needing to make some function definitions available, without actually having to provide a working implementation.

Because todo! diverges (it panics), the compiler is able to use it as a placeholder expression for most types, hence the reference to type analysis in the docs. A liberal use of todo allows deferring some code paths (and not just errors) to be implemented at a later time.

Digression: not to be confused with unimplemented!()

I saw at least once someone being wrong on the Internet about todo!(): they would state that it was meant to communicate to users of a program that some features were not yet implemented.

Rust actually has a dedicated macro for this use case, and its name is unimplemented!().

todo!() has a different audience: the author of the code and their reviewers. No todos are meant to hit production, they are a WIP Rust amenity only.

mut, unused variables, etc.

The default warnings provided by the compiler are very useful while developing. They will catch:

  • unused bindings
  • needlessly mutable bindings
  • unused results and options

All of these tend to appear when code isn't finished developing. For instance, a function whose implementation consists solely of todo!() will typically have such warnings about their parameters.

⚠️It is important to resist the urge to temporarily fix these warnings during development, by prefixing the bindings with _ or other tricks. You risk handing over the unfinished code if you artifically fix the warnings. Ignoring warnings has its place, but it ain't that!

Leaving a memo: the doc comment trick

It often happens while I'm coding that I think of something that needs fixing or handling, like an edge case or some incomplete/broken logic. In contrast with explicit error handling, this kind of issues is not tracked in the type system, so I usually leave a comment with a reminder to fix the issue, if I don't want a distraction from the logic I'm currently implementing.

This is fine and dandy, but I need to remember to actually go back there after the fact 😅

As a means to do this, I often use a trick where such comments are actually prefixed with a triple slash, as if they were documentation comments.

Rust warns about documentation comments that don't document anything, so putting a documentation comment above a statement generally achieve the goal of generating a warning associated with the issue described in the comment.

Where are my warnings? Limitations of vanilla Rust

These techniques served me well for years (I've been writing Rust for more than a decade now...), however I have my grievances with them.

Generally, the most aggravating issue is that not all of these techniques reliably generate a warning associated with the issue that needs solving before sending the code into production.

Forgetting to remove an unwrap and watching it burst into flames after the release is one of the worst feelings you can get as an engineer 😓

todo! is a frequent and weird offender in this category. As stated earlier, it often results in warnings, but these are indirect and caused by e.g. function parameters going unused or mutation not taking place when it should. Because of this, it can become a bit treacherous, as one is used to often have a warning when using todo! and rely on their absence to hit the merge button.

unwrap and clone are even worse, they never emit warnings, so you have to be extra careful in re-reading to remove them before the final PR.

An additional issue with them is that some of them are meant to reach production. With the "unwrap liberally" strategy outlined in this article, we are conflating the normal use case of unwrap with its "WIP" use case.

Lastly, "TODO doc strings" are kind of a hack, perhaps we could say it is a happy little accident? In this capacity, it has unintuitive failure modes, because doc comments are currently forbidden on expressions, so will cause a compilation error. Doesn't frequently happen, but we also run the risk of using the /// in locations where they actually document something, which isn't exactly nice to ship and won't have the expected warning.

Introducing the wip crate

In response to all these small pains for writing WIP Rust, I decided to develop a crate called wip to make it easier to implement the "warnings for later" strategy.

Mostly it consists of a collection of tools you can use in every context while you're developing Rust.

Using wip

Add wip to your dependencies like you would for any other crate:

cargo add wip

Optionally, you can import the prelude provided by wip in the files where you want to use wip:

use wip::prelude::*;

As wip relies on the deprecation warnings of Rust, I suggest you disable the feature of your editor to strike calls to deprecated functions, otherwise it becomes visually invasive.

wip::wip!, a warning-emitting todo!

The first tool provided by wip is a wip! macro, that works exactly like todo!, but always emits a warning on use. Use it everywhere you'd use todo!, to mark code as unfinished.

Why not todo!?

Earlier versions of wip used the todo naming, however it would prevent us from using the prelude, as Rust doesn't like it when a dependency attempts to shadow an std macro...

unwrap_wip and clone_wip

These methods are implemented as extension traits on Result and Option. When in scope, they work exactly like the regular unwrap and clone, except that they emit a warning on usage.

They also clearly signal that you don't mean for the unwrap/clone to stay in final code! Although, keep in mind it doesn't always end up happening that way...

"after all, why not" meme

wip::fixme!, a non-panicking macro for memos

One of the tools I use the most from wip is the fixme! macro. It is a non-panicking variant of wip!, that doesn't stand for an expression. It works the same as TODO comments, sans the hackish part and with a reliable warning.

Embracing our WIP future

I have been using the wip crate for a few of the my latests PRs, including some of my more unreasonably large ones. Usually I add the wip dependency to crates in the workspace during development, and then remove it when rewriting history before review.

It has been a liberating experience, squarely falling in the category of "I didn't expect to need it that much". It makes focusing on the happy path much more straightforward, while not forgetting things for later.

Buzz lightyear "X everywhere" meme

What's not to love?

Only drawbacks I can think of:

  1. One needs to write enough context when adding a fixme, otherwise they can become hard to fix when coming back much later. This is usually not a problem, as wip are usually contained to (usually) short PRs by a single author (or pair programmed).
  2. Sometimes the number of wip-induced warnings can become a bit discouraging, again especially on larger PRs. Perhaps avoid those 😅

Still, I'm quite sure I'll keep using wip. How do you use warnings during development? What do you think would be some useful additions to the current API?