Top-level await
enables developers to use the await
keyword outside of async functions. It acts like a big async function causing other modules who import
them to wait before they start evaluating their body.
The old behavior
When async
/await
was first introduced, attempting to use an await
outside of an async
function resulted in a SyntaxError
. Many developers utilized immediately-invoked async function expressions as a way to get access to the feature.
await Promise.resolve(console.log('🎉'));
// → SyntaxError: await is only valid in async function
(async function() {
await Promise.resolve(console.log('🎉'));
// → 🎉
}());
The new behavior
With top-level await
, the above code instead works the way you’d expect within modules:
await Promise.resolve(console.log('🎉'));
// → 🎉
Note: Top-level await
only works at the top level of modules. There is no support for classic scripts or non-async functions.
Use cases
These use cases are borrowed from the spec proposal repository.
Dynamic dependency pathing
const strings = await import(`/i18n/${navigator.language}`);
This allows for modules to use runtime values in order to determine dependencies. This is useful for things like development/production splits, internationalization, environment splits, etc.
Resource initialization
const connection = await dbConnector();
This allows modules to represent resources and also to produce errors in cases where the module cannot be used.
Dependency fallbacks
The following example 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.example.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.example.com/jQuery');
}
Module execution order
One of the biggest changes to JavaScript with top-level await
is the order of execution of modules in your graph. The JavaScript engine executes modules in post-order traversal: starting from the left-most subtree of your module graph, modules are evaluated, their bindings are exported, and their siblings are executed, followed by their parents. This algorithm runs recursively until it executes the root of your module graph.
Prior to top-level await
, this order was always synchronous and deterministic: between multiple runs of your code your graph was guaranteed to execute in the same order. Once top-level await
lands, the same guarantee exists, but only as long as you don’t use top-level await
.
Here’s what happens when you use top-level await
in a module:
- The execution of the current module is deferred until the awaited promise is resolved.
- The execution of the parent module is deferred until the child module that called
await
, and all its siblings, export bindings. - The sibling modules, and siblings of parent modules, are able to continue executing in the same synchronous order — assuming there are no cycles or other
await
ed promises in the graph. - The module that called
await
resumes its execution after theawait
ed promise resolves. - The parent module and subsequent trees continue to execute in a synchronous order as long as there are no other
await
ed promises.
Doesn’t this already work in DevTools?
Indeed it does! The REPL in Chrome DevTools, Node.js, and Safari Web Inspector have supported top-level await
for a while now. However, this functionality was non-standard and limited to the REPL! It’s distinct from the top-level await
proposal, which is part of the language specification and only applies to modules. To test production code relying on top-level await
in a way that fully matches the spec proposal’s semantics, make sure to test in your actual app, and not just in DevTools or the Node.js REPL!
Isn’t top-level await
a footgun?
Perhaps you have seen the infamous gist by Rich Harris which initially outlined a number of concerns about top-level await
and urged the JavaScript language not to implement the feature. Some specific concerns were:
- Top-level
await
could block execution. - Top-level
await
could block fetching resources. - There would be no clear interop story for CommonJS modules.
The stage 3 version of the proposal directly addresses these issues:
- As siblings are able to execute, there is no definitive blocking.
- Top-level
await
occurs during the execution phase of the module graph. At this point all resources have already been fetched and linked. There is no risk of blocking fetching resources. - Top-level
await
is limited to modules. There is explicitly no support for scripts or for CommonJS modules.
As with any new language feature, there’s always a risk of unexpected behavior. For example, with top-level await
, circular module dependencies could introduce a deadlock.
Without top-level await
, JavaScript developers often used async immediately-invoked function expressions just to get access to await
. Unfortunately, this pattern results in less determinism of graph execution and static analyzability of applications. For these reasons, the lack of top-level await
was viewed as a higher risk than the hazards introduced with the feature.
Support for top-level await
- Babel: no support
- Chrome: no support
- Firefox: no support
- Node.js: no support
- Safari: no support