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

async/await is nearly here and i couldn't wait

Rewriting a futures-0.1 combinator service on nightly async/await ahead of the 1.39 stabilisation, and what the code looked like before and after.

Rust source code on a screen

The stable release with async/await is days away now, 1.39 is due on the 7th, and I have not had the patience to wait for it. The little service I've been carrying on futures 0.1 has been rewritten on nightly, and the diff is the most satisfying thing I've produced in months.

If you wrote anything with futures 0.1, you remember the combinator soup. Every bit of branching turned into and_then, map_err, or_else, all nested, all returning boxed trait objects because the concrete future types were unnameable. Error handling meant threading the error type through every link by hand. It worked, it was fast, and it was miserable to read three weeks later.

Here is roughly what one handler used to look like:

fn handle(req: Request) -> Box<dyn Future<Item = Response, Error = Error>> {
    Box::new(
        fetch_user(req.id)
            .and_then(|user| {
                load_prefs(user.id).map(move |prefs| (user, prefs))
            })
            .and_then(|(user, prefs)| {
                render(user, prefs)
            })
            .or_else(|e| {
                Ok(error_response(e))
            }),
    )
}

And here is the same thing rewritten:

async fn handle(req: Request) -> Response {
    match try_handle(req).await {
        Ok(resp) => resp,
        Err(e) => error_response(e),
    }
}

async fn try_handle(req: Request) -> Result<Response, Error> {
    let user = fetch_user(req.id).await?;
    let prefs = load_prefs(user.id).await?;
    Ok(render(user, prefs).await?)
}

Rust async code

That is just code now. The ? operator works, the control flow reads top to bottom, and the borrow checker understands what's alive across each await point so I can hold a reference across one without fighting the type system. The boxing is gone too: async fn produces a state machine the compiler generates for you, sized and stack-allocated, no trait object unless you ask for one.

It is not all sunshine. The ecosystem is mid-migration and it shows. Tokio's 0.2 with the new traits is still on alpha releases, and bridging a 0.1 library into a 0.3 futures world means compat shims and the occasional afternoon lost to a type error a page long. Pin is a real concept you now have to hold in your head, and the error messages around it are not yet kind. But these are teething problems, not design problems.

The shape of the language changed this week, even if the stable stamp arrives on Thursday. Asynchronous Rust stops being a thing you tolerate for the performance and becomes a thing you can simply write. I rewrote everything, I would do it again, and I'm genuinely looking forward to deleting the last of the boxed futures the moment the dependencies catch up.