Home > Mobile >  Because I can't run await on the top level, I have to put it into an async function - why can I
Because I can't run await on the top level, I have to put it into an async function - why can I

Time:03-18

I have a short Node.js script where I require another package and call an async function from it and subsequently want to print the return value. If I simply await the return value from the top level, then I'll get an error, saying that I can only use await inside an async function itself. So apparently the way to go is like this:

async function main() {
  foo = await someOtherAsyncFunc();
  console.log(foo);
}
main()

Or:

(async function() {
  foo = await someOtherAsyncFunc();
  console.log(foo);
})();

Or:

(async () => {
  foo = await someOtherAsyncFunc();
  console.log(foo);
})();

(Credit to VLAZ in chat https://chat.stackoverflow.com/transcript/message/54186176#54186176)

This works - but I want to understand the reasons behind it a little bit more: I'm used to not being able to directly use await from the top level. However, I'm also used to having to call some special library function to actually "venture" into async from the top level. In Python, see asyncio.run for example. What's the point of requiring await to be inside an async function - if I can then call just any async function from the top level? Why then isn't await available at top level, too?

CodePudding user response:

Top-level await used to not be a thing, but it is possible now in ES6 modules.

One reason why top-level await used to not be a thing, and is still not a thing outside of modules is that it could permit syntactical ambiguity. Async and await are valid variable names. outside of modules. If a non-module script permitted top-level await, then, short of re-working the specification (and breaking backwards compatibility), there would be circumstances when the parser couldn't determine whether a particular instance of await was a variable name, or was used as the syntax to wait for the Promise on its right-hand side to resolve.

To avoid any possibility of ambiguity, the parser, when parsing a section of code, essentially needs to have flags that indicate whether await is valid as an identifier at any given point, or whether it's valid as async syntax, and those two must never intersect.

Module scrips permit top-level await (now) because the use of await as an identifier has always been forbidden in them, so there is no syntactical ambiguity.

In contrast, there are zero issues with using .then on the top level because it doesn't result in any ambiguity in any circumstances.

Why doesn't it just return a Promise which is never executed because it doesn't get awaited?

Promises aren't really "executed". They can be constructed, or waited on to fulfill, or waited on to reject. If you have a Promise, you already have some ongoing code that will (probably) eventually result in a fulfillment value being assigned to the Promise.

Hanging Promises are syntactically permitted - values that resolve to Promises but which aren't interacted with elsewhere. (Which makes sense - every .then or .catch produces a new Promise. If every Promise had to be used by something else, you'd end up with an infinite regress.)

Doing

(async () => {
  foo = await someOtherAsyncFunc();
  console.log(foo);
})();

is essentially syntax sugar for

someOtherAsyncFunc()
  .then((foo) => {
    console.log(foo);
  });

There's no need to tack anything else onto the end of either of those. (though it's recommended to add a .catch to a dangling Promise so unhandled rejections don't occur)

  • Related