Top Level Await Now In V8 But Might Not Be What You Think
Written by Ian Elliot   
Wednesday, 22 January 2020

One of the irritations of JavaScript's wonderful async and await approach is that you have to use it in a function. This is a limitation hat seems to be about to go away when you read headlines like "Top level Await Implemented In V8". However, you need to read the small print.

javascript image

You can't put an await into the top level of the "main" JavaScript program because the code is usually executed in synchronous style. That is, a standard JavaScript program runs all of the code at the top level and then the thread moves to the event queue and starts processing events. You can be 100% sure that the top level code finishes before any events are handled and then running goes fully async, jumping about to service event handlers and callbacks. If you were to put an await in the top-level code then the evaluation of the top-level code would halt and the thread would be freed to process the event queue. This would mean that events started to be handled before the top-level code had completed. Apparently this is a bad idea, even if I think it would be managable in new code.

In case you hadn't noticed, the top-level code runs to completion even if you call an async function that does an await. What happens is that the async function runs until it hits the first await when it pauses and frees the thread. The thread doesn't go off to the event queue; instead it continues with the instruction after the function and runs the top-level code to completion. If you were to put the await in the top-level code it would have to pause and the only thing the thread could do is run the even queue, but by putting it in a function you don't stall the top-level code.

So how does this square with the news that now ECMAScript has a top-level await implemented in V8, Babel and Webpack, and coming to a browser near you quite soon? You can indeed use await in top-level code but only if that code is within a module. You still cannot put await into your "normal" top-level code. What this means is that, if you don't use modules, you still cannot put an await into your top-level code. Notice, however, that loading a module causes its top-level code to be executed as if it was part of the top-level code that is importing it. The module's code is completed before the importing code continues. This means that modules that are loaded one after another and run their top-level code one after another. This is a longstanding principle, but now things are slightly different.

Things started to get complicated when dynamic import was introduced. This brought with it the import function which returns a Promise that resolves to the objects exported in the module. Notice that this introduces some async into the top-level code. You can dynamically load a module, setup the Promise's then method and the top-level code will run to completion and the Promise will resolve sometime later. Notice that the Promise always resolves after the top-level code completes. You can also use dynamic import within other modules and so we now have a situation where modules can complete their code before modules that they import have completed.

You can now use a Promise to asynchronously load modules, but you still can't await a module. The reason is still that an await in the top-level code would would mean that it had to wait and would not complete before the event queue was processed. However, there is no such problem if the top-level code in a module is allowed to await a Promise. In this case the thread would be freed to continue the execution of the top-level code that was importing the module and, as this makes its way back up the import nesting, the top-top-level code will complete because it still cannot have an await within it. This whole idea applies to static import now as well as dynamic. It is as if a static import also created a Promise.

This means that the order of execution of top-level module code is no longer 100% synchronous. Each imported module, whether static or dynamic, will execute until it reaches an await and then it will pause, releasing the thread to continue in the code that was importing it.

Consider the following example from the proposal:

// x.mjs
await new Promise(r => setTimeout(r, 1000));
// y.mjs
// z.mjs
import "./x.mjs";
import "./y.mjs";

Here we have two modules x and y that print something on the log. Without the await in module x you would expect module x to run to completion and print X1, followed one second later by X2 and then y would be imported and it prints Y. With the await what happens is different. Module x prints X1, then does an await and frees the thread which starts the import of module y which then prints Y. After one second, and after all top-level code has completed, the Promise is resolved and the thread continues module x after the await and so prints X2.

Without await we see X1, X2, Y and with await we see X1, Y, X2.  We clearly are going to have to be careful about the order in which things happen, but notice that there is no race condition - with the await things always happen in the same order.  Also notice that the await is in the top-level code within a module and not in the script.

What all this means is that the much discussed top-level await isn't really the top-level await that we all might have been expecting. For the foreseeable future we will still have to put awaits in functions - even if they are immediately executed. Again the reason is that you don't want to stall the top-level code with an await - it has to run to completion to start the event handling loop. It also guarantees that the await only restarts after all of the top-level code completes.

There are some well known criticisms of having a top-level await that brings the top-level code to a halt, but if we change the rules so that an await in top-level code frees the thread to process the event queue, I can't see that there is a problem, although this isn't simple.



More Information

tc39 / proposal-top-level-await

Top-level await is a footgun

Related Articles

JavaScript Async - Basic Async & Await

Rust Gets Async-Await

To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.



Microsoft Goes All Out On Generative AI

Over recent days, Microsoft has announced both the official OpenAI library for .NET and the AI Toolkit for Visual Studio Code.

Mirascope-Python's Alternative To Langchain

Mirascope is a Python library that lets you access a range of Large Language Models, but in a more straightforward and Pythonic way.

More News

C book



or email your comment to:




Last Updated ( Wednesday, 22 January 2020 )