Why Rust’s postfix await syntax is good

The other day on Twitter Kat Marchán said this:

And Jan Lehnardt asked:

I didn’t know of any, and a little searching didn’t turn one up. So here’s one? I hope? If you are not programming Rust a lot, and want to know why not-language-designers like me think that Rust’s await syntax is good, this is the blog post for you.

While looking around for somebody explaining why this is nice syntax, I found one of the discussions about possibilities before it was selected. That’s a pretty long conversation, and I enjoyed skimming it. This comment examining what Rust might look like with a number of the syntax possibilities was particularly neat. It immediately jumped out to me that the one they landed on (postfix field) and the close relation to it (postfix method) felt more Rust-y. But why? You might have to be a Rust user already to feel that.

So in order to explain why Rust’s .await is a nice bit of syntax, I will start by explaining two other things: how chaining calls is idiomatic Rust, and how error propagation with another nice bit of syntax, ?, supports this.

Hoo hah back on the chain gang

Chaining is a very common idiom for taking one collection and transforming it into another, perhaps even one of a different type. This snippet takes a collection of id-having-things (any collection type, so long as it is iterable), iterates through them, plucks out the ids, and re-collects them into a Vec:

let ids: Vec<usize> = things_with_ids.iter().map(|xs| xs.id).collect();

Here’s a slightly-edited real-world example, which chains some stuff to end up with a string:

let malformed_kinds = requested_kinds
    .iter()
    .filter(|xs| !is_valid(xs))
    .cloned()
    .collect::<Vec<_>>()
    .join(",");

And the idiom is seen in other areas of API design. Here’s how my little Skyrim mod tool sends posts (with some editing to make it a useful example):

let agent = ureq::AgentBuilder::new()
    .timeout_read(Duration::from_secs(50))
    .timeout_write(Duration::from_secs(5))
    .build();
let maybe_response = agent
    .post(uri)
    .set("apikey", &self.apikey)
    .set("user-agent", "modcache: github.com/ceejbot/modcache")
    .send_form(body);

All of this is to say: chaining like this is common in Rust.

Don’t break the chain

Now, there isn’t any error handling visible in the above code. What does error handling look like with chaining? Does it break the chains? It used to! The ? error propagation symbol is new to Rust since I first started using it, and its introduction has made writing error handling a lot nicer.

Rust allows you to express that an operation might fail by returning a Result. This is a sum type:

enum Result<T, E> {
   Ok(T),
   Err(E),
}

If all went well, you get the Ok variant with your data in it. If it did not, you get the Err variant with your error type. The Rust compiler makes you handle both variations in your code.

Here’s a faked example of getting some data from a function that might fail, and doing something with that if we can.

// Our fetch talks to a db so it might fail for reasons
// beyond our control, so we return a result type.
fn fetch_all_animals() -> Result<Vec<Animal>, SomeErrorType> {
    // blocking call to a db here
}

// We depend on a fallible function, so we are fallible too.
fn count_hedgehogs() -> Result<usize, SomeErrorType> {
    // this is a Result
    let maybe_animals = fetch_all_animals();
    //... so we match on it to see if we succeeded or not
    match maybe_animals {
        Ok(animals) => {
            // we got some animals! let's find the hedgies
            let count = animals.filter_for_hedgehogs().len();
            Ok(count)
        }
        Err(e) {
            // We failed to get animals. We handle the error in whatever
            // way makes sense for the program. Here we just propagate
            // the error on up to the caller.
            Err(e)
        }
    }
}

This error handling pattern was everywhere in my Rust code, being verbose all over the place. It’s also predictable! This makes it a good candidate for sugar. So the ? syntax for this was added in Rust v1.13 at the end of 2016. If all you want to do is return immediately if you have an error and carry on if you got an OK result, use ?.

fn count_hedgehogs() -> Result<usize, SomeErrorType>
{
    let animals = fetch_all_animals()?; // <-- note the ?
    // if the fallible function failed, we have bopped that
    // error on out & can proceed
    let count = animals.filter_for_hedgehogs().len();
    Ok(count)
}

You can see that error handling is a lot less verbose when it can fit into this pattern. In fact, the idiomatic Rust way to implement the above function is to chain it all together:

let count = fetch_all_animals()?.filter_for_hedgehogs().len();

Which is super-compact and might not need its own function at all. This stays super-compact if our hedgehog filter is fallible as well, though I’m not sure why it would be fallible. It would look like this:

let count = fetch_all_animals()?.filter_for_hedgehogs()?.len();

Finally we get to async and await

Now! Let’s suppose we have moved to the magic land of async Rust programming and have a non-blocking db fetch for our animals.

// we must say the magic word
async fn fetch_all_animals() -> Result<Vec<Animal>, SomeErrorType> {
    // we do all the same work as before
    // and maybe call some async functions here too
}

Now when we call that function, what we get back is actually a Future. To use it, we have to call poll on it, or more idiomatically, we await it to resolve it to a value. (There’s a link in the further reading section if you want to learn more.) This is a lot like what happens in Javascript when we get a promise back from an async function:

const animals = await fetch_all_animals();

But Rust’s chosen syntax uses a field-like postfix on a Future, and this is the specific thing I think is neat:

let animals = fetch_all_animals().await;

Look at what happens if we’re calling fallible functions and want our error handling in-line! We stick ? on the .await to propagate any errors and unwrap a result in-line:

let count = fetch_all_animals().await?.filter_for_hedgehogs().len();
// and if our hedgehog filter were both async and fallible....
let count = fetch_all_animals().await?.filter_for_hedgehogs().await?.len();

That is the use case that shows why I think this specific syntax choice is brilliant. Precedence is clear. We don’t have to wrap things in parens for human readability or to control precedence. If we read a chain, the operations are mentioned in the order that they happen. It works with the existing idioms rather than against them.1

Another thing that’s interesting to me here is that this choice is not what most modern languages made for their syntax. Lots of them use a prefixed await keyword. In javascript if we were chaining it would look like:

const count = (await fetch_all_animals()).filter_for_hedgehogs().length;
// and errors will throw exceptions that we're letting bubble up
// and if we're chaining more than one async thing...
const count = (await (await fetch_all_animals()).filter_for_hedgehogs()).length;

But I’d probably never write either of those and definitely never the second. I am far more likely to write:

let count = 0;
try {
    const animals = await fetch_all_animals();
    count = animals.filter_for_hedgehogs().length;
} catch (ex) {
    // handle the error at this level
    // I'd omit the try/catch if I wanted the error to propagate
}

My aversion to chaining partly comes from the fact that I must use the parens to express my intent. The syntax of any programming language shapes what code feels idiomatic and most readable and what code feels like patting a cat tail to head. There’s nothing right or wrong about any of it, because all of them have found a way to express the concept.

Is postfix await a small bit of syntax? Yes. Is it thoughtfully chosen out of many possibilities? Yes. Is it very much in tune with the Rust syntax around it? Also yes. This is what I appreciate most about the Rust project: its concern for the experience of the human beings using the language.

Further reading

If you would really like to understand Rust futures, you should read @fasterthanlime’s article “Understanding Rust futures by going way too deep”.

If you are comfortable reading Rust, and want to know more about async executors and how they work, check out whorl. This repo walks you through the implementation of an async executor and shows you what await desugars to. (Hat tip to Chris Dickinson for telling me about this!)

If you have any pointers to other posts about why this syntax is neat, please send them to me and I will link! Also, if you have your own reasons about why this syntax is nice, please do write them up and I will link those too!


  1. You might say that the people who chose the await syntax had a strong hold on the theory of Rust↩︎

Ceejbot

This internet thing seems to have taken off.


By C J Silverio, 2022-05-13