I had a little service held together with futures combinators: chains of .and_then() and .map() that I'd written back when that was the only way to do async Rust. It worked. It was also unreadable. Every error path needed a .map_err() bolted on, the type signatures grew to a paragraph each, and the actual logic was buried under the plumbing of stitching futures together by hand.
async/await has been stable for a while now, and I'd been meaning to migrate. This week I finally did, and the diff was a joy. Whole combinator chains collapsed into straight-line code that reads top to bottom:
async fn fetch_user(id: u64) -> Result<User, Error> {
let row = db.query_one(id).await?;
let prefs = cache.get(id).await?;
Ok(User::from(row, prefs))
}
That .await? is the whole thing. Await the future, propagate the error with ?, carry on. No nested closures, no fighting the borrow checker over what moved into which combinator. The compiler builds the state machine; I get to write what looks like blocking code that isn't.
I rewrote more than I strictly needed to, because it was that satisfying. Sometimes a language feature doesn't just let you do new things, it lets you delete things, and deleting code you used to need is one of the better feelings in this job.