The top-level await
's ECMAScript proposal, which reached Stage 3 last year, is now implemented in the V8 JavaScript engine, and supported by Webpack and Babel. Top-level await
enables dynamic dependency pathing, resource initialization, and dependency fallbacks at module import time. Top-level await,
however, results in a non-deterministic module execution order.
Top-level await
(TLA) enables developers to use the await
keyword outside of async functions. With top-level await
, the above code, at the top level of a ES module, becomes valid code:
await Promise.resolve(console.log(''));// →
The specification of the proposal distinguishes three use cases. The first use case is dynamic dependency pathing:
const strings = await import(`/i18n/${navigator.language}`);
This allows modules to determine dependencies dynamically at runtime. The proposal mentions:
This is useful for things like development/production splits, internationalization, environment splits, etc.
The second use case is resource initialization. Axel Rauschmayer provides the following example of a module fetching asynchronously from two sources the resource which loads fastest:
const resource = await Promise.any([
fetch('http://example.com/first.txt')
.then(response => response.text()),
fetch('http://example.com/second.txt')
.then(response => response.text()),
]);
In this context, TLA will also allow to better handle errors in case of unavailable resources.
The third use case is related to dependency fallbacks. The proposal provides the following example of a module which attempts to load a JavaScript library from CDN A, falling back to CDN B if that fails:
let jQuery;
try {
jQuery = await import('https://cdn-a.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.com/jQuery');
}
In the absence of TLA, developers may address these use cases with workarounds involving immediately invoked arrow function expressions (IIAFEs). Rauschmayer detailed in an article why these workarounds are not good enough, which in return underscores the value of the Stage 3 proposal.
TLA however breaks the current deterministic module execution order. As a matter of fact, the JavaScript engine executes modules in post-order traversal, and guarantees the same ordering across runs. When TLA is used, the module execution order will depend on the asynchronous resolution of the modules using TLA declarations. This introduces some risks, in particular that of deadlocking (for instance when dealing with circular module dependencies). The proposal, however, guarantees that when the TLA syntax is incorporated in JavaScript, if that syntax is not used, then the deterministic post-order traversal will continue to apply.
TLA was promoted to Stage 1 in January 2018, Stage 2 in May 2018 and Stage 3 in June 2019. TLA has recently been implemented in V8 (behind a flag). Babel 7.7 now parses the TLA syntax. Webpack 5 also now supports the feature behind a flag.
The full proposal can be consulted in the TC39 GitHub repo.