Await postfix operator(@)

Summary

Add "@" as await postfix opoerator("at sign" means "await") to let code looks more like synchronous.

"@" works like below:

asyncFunction()@

it's equivalent to

(await asyncFunction())

Use cases

  1. Take a look at this example. (from Google/zx)
  • With "@" before
    #!/usr/bin/env zx
    
    await $`cat package.json | grep name`
    
    let branch = await $`git branch --show-current`
    await $`dep deploy --branch=${branch}`
    
    await Promise.all([
      $`sleep 1; echo 1`,
      $`sleep 2; echo 2`,
      $`sleep 3; echo 3`,
    ])
    
    let name = 'foo bar'
    await $`mkdir /tmp/${name}`
    
  • With "@" after
    #!/usr/bin/env zx
    
    $`cat package.json | grep name`@ //await here
    
    let branch = $`git branch --show-current`@ //await here
    $`dep deploy --branch=${branch}`@ //await here
    
    Promise.all([
      $`sleep 1; echo 1`,
      $`sleep 2; echo 2`,
      $`sleep 3; echo 3`,
    ])@ //await here
    
    let name = 'foo bar'
    $`mkdir /tmp/${name}`@ //await here
    
  1. Get the result of a promise without parenthesis.
  • With "@" before
    let asyncCalculateResult = (await asyncCalculator.plusAsync(100)).calculateResult
    
  • With "@" after
    let asyncCalculateResult = asyncCalculator.plusAsync(100)@.calculateResult
    
  1. Simplify asynchronous for...of syntax.
  • With "@" before
    for await (let number of getAsyncSequenceNumbers(10)) {
        //do something
    }
    
  • With "@" after
    for (let number of getAsyncSequenceNumbers(10)@) {
      //do something
    }
    

Why choose "@"?

To protect our eyes

I found there is other languages choose await postfix keyword.

Take a look at this asynchronous method chaining.

await (await (await (await asyncCalculator.plusAsync(1)).minusAsync(2)).multiplyAsync(3)).divideAsync(4);
  • With ".await()"
    asyncCalculator.plusAsync(1).await() .minusAsync(2).await() .multiplyAsync(3).await() .divideAsync(4).await() .calculateResult
    
  • With "@"
    asyncCalculator.plusAsync(1)@ .minusAsync(2)@ .multiplyAsync(3)@ .divideAsync(4)@ .calculateResult
    

It is difficult to read when too much characters in the method chaining,
"@" can split them and be highlighted by code editor to group the asynchronous functions.

Other benefit

To protect our fingers and keyboard

  • With "@" before

    let task = await getAsyncTask();
    

    We need to press "a" key -> "w" key -> "a" key -> "i" key -> "t" key -> Space key = pressing key 6 times.
    If you don't want to wait it, you need to move back to "await" and delete it.

  • With "@" after

    let task = getAsyncTask()@;
    

    Only press "2" key + Shift key = pressing key once. (It can be auto completed by code editor.)
    If you don't want to wait it, you only need to press Backspace key once.

How to try it out

If you have VS Code, you can install this demo extension to try it out.

About this idea

It is inspired by the discussion of async deference operator.

Some related ideas here if you haven't seen before:

Where it would look like:

let result = await asyncCalculator.plusAsync(1)~.minusAsync(2)~.multiplyAsync(3)~.divideAsync(4)~.calculateResult;
1 Like

I actually like the weight of the await keyword, and it's position as a prefix, as it represents an interleaving/suspension point that shouldn't be glossed over.

The nesting readability concern is greatly reduced with pipelines, and await is one of the motivating use cases (and one of the reason why hack pipes were favored).

The wavy dot proposal is actually a lot more complex than simply awaiting on intermediate results as it allows pipelining. It's likely that a combination of pipeline and runtime helpers would sufficiently reduce the need for wavy dot, which is a reason why we haven't pushed it further for now.

5 Likes

@aclaymore's referenced proposal would directly solve this issue, and it would be nice to see something like that added. As for what can be done today, I've started making it a habit to just use .then() instead of parentheses whenever I need to nest awaits. It generally makes the result a little cleaner.

Some examples:

// before
await (await fetch('https://example.com')).text()

// after
await fetch('https://example.com').then(x => x.text())

// before
await (await (await (await asyncCalculator.plusAsync(1)).minusAsync(2)).multiplyAsync(3)).divideAsync(4);

// after
await asyncCalculator.plusAsync(1)
  .then(x => x.minusAsync(2))
  .then(x => x.multiplyAsync(3))
  .then(x => x.divideAsync(4))

Also, for this point:

To protect our fingers and keyboard

In general, it's better to optimize for readability over writability, as code is read much more often than it is written. So, while this argument is still a valid one, it's generally put really low on the list of priorities. It's often overshadowed by things like: how readable is this new operator over the old one? (which, I'd argue if we're not nesting, the await operator is more readable, but that's really a matter of taste). Do we really need to use one of our few precious ascii symbols for this? If we introduce too many operators like this, will JavaScript just turn into an ascii soup that's difficult to understand? etc.

Edit: Just saw @mhofman's comment. Nested awaits have always been a bit of a pain point for me. When I started using the .then() trick, it helped out a lot. I didn't think about how pipelines would fix this, but yeah, I guess that would help fix the issue a lot - to the point where I wouldn't really feel a desire for this sort of feature anymore. But, I guess pipelines aren't that much different from just using .then() as well, which might be why I found that to be so helpful.

1 Like

Hi, @aclaymore ,
Thank you for your information!

"~" looks like the liquid in pipeline flows to another side, it's nice.

Hi, @mhofman ,
Thank you for your opinion!

The pipeline operator "|>" looks more intuitive thant "~", it's great.

Hi, @theScottyJam ,
Thank you for your sharing.
That's awesome!
It looks simple, easy to understand and useful.
I really learned from your example.

1 Like

Chaining method calls is so easy to use,
so I created a tiny library to convert synchronous nested method calls to chaining method calls.

@tzengshinfu - careful there. It's generally recommended not to modify the prototypes of built-in objects, and that's done for good reason - you might add a function that conflicts with current or future JavaScript functions, or that conflicts with other libraries. (Every time JavaScript tries to add a function, they generally have to look around to make sure their function doesn't cause issues with any libraries that have modified built-in prototypes.

In this case, your library actually conflicts with an existing protocol - the "thenable" protocol. Promises will chain with anything that has a then function

This means:

> Date.prototype.then = () => console.log('hi there')
> await Promise.resolve().then(() => new Date())
hi there
(script never terminates)

Or, if I run this line of code after loading your library in, the promise will never resolve and the script will never terminate.

> console.log(await Promise.resolve().then(() => new Date()))
(script never terminates)

Part of the issue here is the fact that that ES6 chose to make anything with a then() function implement this protocol (as opposed to using symbols), which can cause surprising effects to anyone who adds a "then()" function to their objects. But, the fact that you're modifying prototypes means existing code that tries to return a Date object or what-not from a promise will break, which makes things even worse.

You could just fix this by renaming the function, but my personal recommendation would be to do something that you might not like, because it's a bit more verbose. (This is how lodash solves the problem).

import chain from 'your-library'

return chain(2 + 2) // Returns an object that holds the internal value, and has a .then() function
  .then(x => x + 1) // Or call it next() or something if you want this to not be then-able.
  .then(x => x * 2)
  .value() // Gets the internal value out of this wrapper object
1 Like

Hi, @theScottyJam ,
Thank you for your explaining, I learned a lot from you again.

Now I got it, Thenable protocol, Duck typing, and don't pollute the prototypes of built-in objects.
I will build a wrapper object to chain it's mehods.

1 Like