Understanding Top-Level Await in JavaScript Modules
This article explains how top‑level await, introduced in V8 from version 9.1 and enabled by default in modern browsers, allows developers to use await directly at a module’s top level, simplifying asynchronous code patterns with examples, execution order details, and practical use cases.
From v9.1 onward, V8 enables top‑level await by default, even without the --harmony-top-level-await flag; Blink v89 also enables it by default.
In the Blink rendering engine, version v89 defaults to top‑level await .
What is top‑level await
Previously, await could only be used inside an async function; using it at a module’s top level caused a SyntaxError, forcing developers to wrap code in an immediately‑invoked async function.
await Promise.resolve(console.log('🎉'));
// → SyntaxError: await is only valid in async function
(async function() {
await Promise.resolve(console.log('🎉'));
// → 🎉
})();Now the module’s top level can directly use await, making the whole module behave like a large async function.
await Promise.resolve(console.log('🎉'));
// → 🎉Note: top‑level await is only allowed in ES modules; traditional script tags or non‑ async functions cannot use it.
Why introduce top‑level await
A common scenario involves a utility library and middleware that need to perform asynchronous initialization before their exported values are usable.
Utility library module
//------ library.js ------
export const sqrt = Math.sqrt;
export function square(x) { return x * x; }
export function diagonal(x, y) { return sqrt(square(x) + square(y)); }Middleware
The middleware imports the utilities and waits for an asynchronous operation before computing results.
//------ middleware.js ------
import { square, diagonal } from './library.js';
console.log('From Middleware');
let squareOutput;
let diagonalOutput;
(async () => {
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
})();
function delay(ms) {
return new Promise(resolve => {
setTimeout(() => {
console.log('❤️');
resolve();
}, ms);
});
}
export { squareOutput, diagonalOutput };Main program
The main script imports the middleware’s exported values, but they are initially undefined until the asynchronous work finishes.
//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';
console.log(squareOutput); // undefined
console.log(diagonalOutput); // undefined
console.log('From Main');
setTimeout(() => console.log(squareOutput), 2000); // 169 (after await)
setTimeout(() => console.log(diagonalOutput), 2000); // 13 (after await)Solution with top‑level await
By using top‑level await in the middleware, the module pauses execution until the asynchronous initialization completes, allowing the main program to receive ready values without extra wrappers.
//------ middleware.js (with top‑level await) ------
import { square, diagonal } from './library.js';
console.log('From Middleware');
let squareOutput;
let diagonalOutput;
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);
function delay(ms) {
return new Promise(resolve => {
setTimeout(() => {
console.log('❤️');
resolve();
}, ms);
});
}
export { squareOutput, diagonalOutput };
//------ main.js ------
import { squareOutput, diagonalOutput } from './middleware.js';
console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log('From Main');In the above code, main.js waits for middleware.js to resolve its await before proceeding, simplifying the flow.
Other use cases
Dynamic import of dependencies
const strings = await import(`/i18n/${navigator.language}`);Resource initialization
const connection = await dbConnector();Dependency fallback
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
Using await changes the module graph traversal: the engine performs a post‑order traversal, executing child modules first, exporting bindings, then parent modules. With top‑level await, a module pauses until its own awaits settle before its parent continues.
Execution of the current module waits for its top‑level await to finish.
Child modules must finish their awaits and export bindings before the parent module runs.
If there are no other awaits, sibling and parent modules continue in the same synchronous order.
After an await resolves, the awaiting module resumes execution.
Without additional awaits, the tree proceeds synchronously.
Common questions
Does top‑level await block execution? No, sibling modules can still run; it only pauses the module that contains the await.
Does it block resource requests? No, resource fetching starts during the module graph linking phase, so top‑level await does not delay network requests.
What about CommonJS modules? Top‑level await is only supported in ES modules; it does not work in script tags or CommonJS.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
