Update 2023: I'm happy to say that the syntax selected has proven itself as a good choice, and getting used to it really wasn't as much of a pain point as I had anticipated. I'm going to leave my original post here for reference, but know that I think the direction taken by the Rust team turned out to be a good one :)

This post will be an extension of the debates found in the Rust forums, specifically here and here. A lot is being said in those threads and there's a certain amount of duplicated posts that are drowning out potentially valuable information and perspectives. I've written several comments in those debates which have received positive feedback, so this post is intended to re-broadcast those thoughts to the wider Rust community in case there's anything of value in them. At the very least I'll be happy that I've written my thoughts somewhere; this post is as much for me as it is for anyone else.

If anyone from the language team makes it to this post, please know that even if I disagree with your personal opinions/choices in this post, I appreciate all the time you've spent on this topic (and all other topics!).

A little Background

The debate at hand is the concept of "async/await" in Rust, which exists in the Nightly builds but does yet not have a finalized syntax. Until recently, it was assumed that we would use the familiar prefix-based syntax found in many other languages:

let value = await future;
let value = await my_async_function();

It came to light recently in a write-up of the discussions so far that this syntax has a large disadvantage in terms of ergonomic Rust, in that it doesn't play nicely with the ? operator. This is described in the mentioned write-up, but for all intents and purposes, means that:

// this would not work as expected
let value = await future?;
let value = await my_async_function()?;

// this would, but isn't attractive
let value = (await future)?;
let value = (await my_async_function())?;

The intent of the "re-design" of await syntax is to figure out a more ergonomic way to go about this use case, as ? is encouraged in ergonomic Rust. There are many options, but I won't visit them here as they're discussed at length by the language team in better detail than I would provide. The current favoured solution for the "official" syntax is discussed by Boats in a post and involves a new "postfix" syntax which allows for interoperation with the ? operator as normal:

let value = future.await?;
let value = my_async_function().await?;

Also mentioned is that this syntax could be used in other cases, with an example shown being the match operator - which makes this syntax a little more compelling. If await uses a feature that is more general to the single use case, there's more weight in the argument to introduce it using that feature.

There are a lot of other proposed solutions around the internet, but as this syntax comes from the language team and seems to be slowly moving to becoming the solution, I'm going to assume that this is the syntax to base this post on.

Revising the Problem

Now that we have a final proposal, we should revisit the problem we're trying to solve. It seems that nobody (at least publicly) is comparing the new proposals against the original plan, only against other proposals. The initial problem was the unattractiveness of having to wrap your await usage in parentheses in the case you want to bubble up your errors. As an exercise, let's place the two proposals next to each other:

// initial proposal
let value = (await future)?;
let value = (await my_async_function())?;

// most recent proposal
let value = future.await?;
let value = my_async_function().await?;

Stare at them for a few seconds and it raises an interesting question: is the new proposal any better than the problem we're trying to solve? Keeping in mind that the initial problem essentially boils down to unattractiveness, whether this is an improvement or not really needs to be carefully considered before making a final decision.

There are two scenarios to look at here; the first is to see how the new proposal fairs when used alongside the ? operator (as I have done above). The second is one that a lot of people have missed on the forums (in many proposed solutions); looking at the situation without the ? operator. So, we'll list that below as well:

// initial proposal
let value = await future;
let value = await my_async_function();

// most recent proposal
let value = future.await;
let value = my_async_function().await;

One thing to note here is that I believe people are overestimating how frequently people bubble errors straight up without any transformation steps. For example, in many cases errors are placed into enums and not immediately passed back up. Therefore, we should also consider code that looks like this (which is yet another familiar pattern in ergonomic Rust):

// initial proposal
if let Err(e) = await future {

}

// most recent proposal
if let Err(e) = future.await {

}

When looking at improving the attractiveness of code, we need to make sure that there is a net improvement for all code involved, not one particular use case. If there's an improvement in one scenario which degrades another scenario, it shouldn't always be deemed as a win. And so, at this point, I'd like to state that I am in favour of keeping the original plan (with some caveats I'll come to later). I want to make this clear, to make my bias clear for the rest of this post.

Net Reduction in Ergonomics

I'll begin this by pointing out that we need to keep in mind that all of this is because we want to avoid wrapping some statements in parentheses. Yes, this might be a blunt way of phrasing it, but it's necessary to clarify that ? can be used with the initial syntax. We're not adding the ability to use ? here, we're simply trying to avoid writing (). I'm not sure I understand why this is assumed to be such a hot issue; we do this in other areas of Rust without causing such a debate.

To compare the syntaxes, we'll start with the basic case of awaiting a future and not bubbling up the result. I would be surprised if there's someone out there who could successfully argue that future.await is a better choice to await future in the case the error is not being passed back to the caller. The syntax is more familiar (even to those who don't write Rust code), and is generally well regarded in the broader programming ecosystem. To me, this means that whatever we come up with to solve our problem must be contrasted with what we're losing in this case. If what we add is a little better for use with ?, but severely degrades the case without ?, it should be a questionable solution at best.

In the case we're bubbling up a result, we come to the hotly debated syntax of future.await? vs (await future)?. Rather than debate this directly, I'd like to simply make a point that opinion in the Rust community is greatly divided on future.await? existing at all. If future.await? is split down the middle in that 50% of people like it and 50% of people don't, you could conclude that it's a reasonable candidate to introduce to the language. However, if you then realise that 90% of people think future.await is worse than await future, it becomes a less reasonable candidate. Obviously these numbers are for demonstration purposes, but they show how we're potentially letting one use case cloud our judgement when it comes to the other use cases.

I don't believe that future.await? brings enough value to switch away from the initial proposal. When you consider whether it's worth switching to this syntax, it has to be worth the engineering time involved, the cognitive cost of learning this new syntax, and the cost of arguing about it for days on end. I'm not sure this slight (arguable) improvement is worth it. After a few days of consideration, I prefer (await future)? simply because I don't see why we need anything else. I find it more readable, both in the snippets above and in natural code itself. The alignment lends to code being more structured in general, and you don't find yourself scanning to the end of a line to figure out if something is synchronous or not (which can be very frustrating). All of these things would impact someone in day-to-day Rust, and I don't feel as though we're gaining enough out of future.await? to warrant the changes.

On Postfix Keywords

Although I disagree with introducing postfix keywords at this point for the await keyword, I do believe that there is benefit in future. I would suggest that we follow the initial plan for await syntax, but revise the postfix version in a general "Postfix Keyword" feature which adds postfix implementations of all keywords in the languages - rather than picking out a single keyword. This allows introduction for all keywords at the same time, which is much more consistent and avoids us rushing the implementation because of the timeline associated with async/await.

It has been mentioned that the language team doesn't want to introduce "throwaway" syntax for await, but in this scenario it would be no more throwaway than the existing match keyword (assuming match was one of those to get a postfix implementation). This gives us the flexibility to choose which approach we want to use in everyday development. If I can choose between match foo { and foo.match { (or however it turns out), I should be able to do the same with the await keyword as it would otherwise be an inconsistency in the language. In practice, this would typically mean you would see code like this:

// not bubbling up errors
let value = await future;
let value = await my_async_function();

// bubbling up errors
let value = future.await?;
let value = my_async_function().await?;

I think this is a reasonable tradeoff for allowing the developers choice in their implementation. At this point with the debate being so passionate over the syntax, I don't see how we could even pick a single syntax without it dividing the community further. This path allows the language team to take more time over something that shouldn't be done on a whim (postfix keywords), allows them to get ? interoperability into the language, and also keeps await future around for those who prefer it. It seems like a solid middle ground, whilst keeping timelines the same for async/await. Separating the introduction of async/await and postfix keywords results in a seemingly much better engineering workflow, with the end result being the same (although with a more familiar await option for those who want it).