Over the last few days I have written some code with about a 300 occurrences of .await
(with the somewhat decent compat
features of futures 0.3) , and I’d like to share some personal observations.
I’ll cover ease of use, observability, oddity factor and language switching cost.
Utility and ease of use
My opinion that a postfix syntax is very desirable has been reaffirmed.
.await
is very easy to type and makes writing code really fluid and convenient.
It prevents the need for introducing unneeded locals, and also avoids falling back to combinators.
With .await
chaining, applying await becomes really effortless and is very nice to write. Something that’s usually not exactly a strength of Rust, with it’s tendency for boilerplate.
For example, in Javascript I would sometimes use Promise
combinators to prevent locals.
// Use of combinators.
async function load() {
return fetch(...)
.then(r => r.json())
.then(response => response.data);
}
// With locals
async function load() {
const res = await fetch(..);
const json = await res.json();
return json.data;
}
The equivalent comparison in Rust would be:
// With combinators
async fn load() -> Result<Value, Error> {
reqwest::get(..)
.and_then(|res| res.json())
.map(|value| value.data)
.await
}
// With locals.
async fn load() -> Result<Value, Error> {
let res = reqwest::get(..).await?;
let json = res.json().await?;
Ok(json.data)
}
// With chaining.
async fn load() -> Result<Value, Error> {
reqwest::get(...)
.await?
.json()
.await?
.data
}
One might argue that it’s almost too easy to use. (See below).
Observability
Combined with how easy it is to apply, I noticed that I really stop thinking about if I really want to suspend here or not.
I just slap a .await
at the end and move on.
This is often fine, but it also can cause problems. I stumbled over two such examples:
- I introduced a bug because I was incrementing a
AtomicU64
after a suspension point, which really needed to be incremented before hand because I appended a.await
on to a function call without thinking about it - I made a particular function too slow because three things that were supposed to happen in parallel happened one after the other, again because I just took the convenient route of
.await
without thinking about it.
You could of course argue that this is just my mindlessness at fault.
But I do think a noisier syntax that looks less like just a field access causes a bit more of a mental stopgap that makes you consider if suspending right there is really what you want.
The noiselessness of the syntax can therefore both be seen as a feature and as a detriment , IMO.
Also, without syntax highlighting, .await
becomes very easy to miss.
This is real annoyance in eg git diff
. .await
looks just like a field access and doesn’t stand out at all. This is alleviated a bit by the fact that .await?
will be very common, which stands out more, but it’s still an issue for me personally.
Even with highlighting, it still is weird for me to associate it with control flow rather than just a cheap field access. This is something that could become better with time, if you are writing a lot of Rust code.
Oddity factor
I did notice that .await
becomes natural to use very quickly. I stopped thinking about it quickly when writing code.
Yet, when reading the code rather then writing, it still looks very odd to me that
value.await
is not just a field access but actually suspends the function.
I just have trouble separating field access from the control flow that await introduces, and I don’t think that is something that would go away with time.
As I mentioned in a previous post, I’m really worried about this syntax creating a permanent weirdness factor in the language, since DOT
can not be a general pipeline operator (unless we also get universal function call syntax).
I still hold the view that DOT
is a very questionable choice for this regardless, and it’s doubtful to me that this syntax could be accepted for other language constructs like .match
, .if
, …
Future consistency of the language should be an important concern.
Also, the syntax will be very odd for users not familiar with Rust or who write little Rust code. (The same would be somewhat true for any kind of postfix syntax though, I reckon) This leads to my last point.
Language Switching
One very real detriment I discovered is switching between languages and familiarity concerns.
While writing code over the last few days, I was often switching between Javascript and Rust, and this was a real headache for me.
I kept trying to apply prefix await in Rust, and after a while, then started to use .await
in Javascript, which the obvious results.
I believe this will be a real issue for developers using multiple languages, because you constantly need to readjust and move code around.
Almost every language that supports async/await
has the same prefix syntax for it, and Rust will be the odd one out. This could become a real annoyance for devs that don’t write Rust often, or write multiple languages with async await on a regular basis.
The mental and familiarity cost is very real and should not be dismissed lightly.
Conclusion
I assume that, with the lang team having consensus and the syntax already being in master, the decision is pretty much made.
Personally, after writing a pretty decent amount of code and extensive consideration, I have to say that it is a decision I can live with, but not one I would personally make.
I still believe introducing the standard prefix syntax, and then working on a postfix pipelining would be the wiser long term choice, due to three primary reasons, mostly outlined above.
- long term language consistency
- clear distiction of concepts (field access vs control flow)
- cross language familiarity (With prefix await available, devs could always use the familiar and common syntax, with a more rusty a postfix variant being available for those that want it)