Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

async/await finally clicked, so naturally i rewrote the lot

After living with futures combinators for a year, moving a small Rust service over to async/await and tokio 0.2 made it readable enough that I rewrote it whether it needed it or not.

A code editor showing a Rust source file mid-edit

I have a small Rust service that fans out to a handful of HTTP endpoints, collects the results, and stitches them together. The original was written in the futures-combinator style: and_then, map_err, Box<dyn Future> everywhere, type errors that filled the terminal and read like someone had sat on the keyboard. It worked. I just never wanted to open it.

async/await has been stable since 1.39 last November, and tokio 0.2 has settled down enough that I finally bit the bullet. The rewrite was almost embarrassing. A combinator chain I had been quietly proud of collapsed into about six lines that read top to bottom like ordinary code:

let a = fetch(&client, url_a);
let b = fetch(&client, url_b);
let (a, b) = tokio::try_join!(a, b)?;
Ok(merge(a, b))

That try_join! runs both concurrently and short-circuits on the first error, which is exactly what my eye-watering join and and_then dance had been doing all along, just without me having to prove it to myself each time I read it.

The honest part: the service did not need rewriting. It was fine. It was fast enough and it had not crashed. I rewrote it because the new version is one I am willing to maintain, and the old one I was not, and that turns out to matter more than I would have admitted a year ago. Readable code is code you actually fix. The borrow checker still has opinions about holding references across an .await, and I lost an evening to that, but the trade is firmly worth it. I am not going back.