What is "top-level await"?
What it was like before
Before top-level await, await could only be used inside an async function:
// An error before
const data = await fetch('/api/data');
// Works
async function main() {
const data = await fetch('/api/data');
console.log(await data.json());
}
main();If you needed to wait for something at the start of a module, you had to create a wrapper function (init(), bootstrap(), and so on).
What changed
With top-level await you can now write:
// module.mjs or type: "module"
const response = await fetch('https://api.example.com/user');
const user = await response.json();
console.log('User:', user.name);The code pauses the module's loading until the promise resolves. This makes the module asynchronous "by nature".
Where you can use it
Only in ES modules (ESM) - that is, in files:
- with the
.mjsextension, or - in
.js, ifpackage.jsonhas"type": "module", or - in
<script type="module">in the browser.
Not in CommonJS (Node.js require).
How it works internally
When a bundler or runtime encounters await at the top level of a module:
- The module becomes asynchronous.
- Its execution pauses until the
awaitresolves. - Other modules that import this module also wait for it to finish executing (through the dependency chain).
Example 1 - importing with a wait
// user.mjs
export const user = await fetch('/api/user').then(r => r.json());// main.mjs
import { user } from './user.mjs';
console.log('User name:', user.name);Here main.mjs does not start running
until user.mjs finishes its await.
Example 2 - dynamic initialization
// config.mjs
const env = await fetch('/env.json').then(r => r.json());
export const API_URL = env.production ? 'https://api.prod' : 'https://api.dev';The config module can now be imported like a regular one,
and it will asynchronously fetch its own config.
Example 3 - top-level await with a dynamic import
// main.mjs
const lang = navigator.language.startsWith('fr') ? 'fr' : 'en';
const messages = await import(`./messages.${lang}.js`);
console.log(messages.default.hello);Lets you dynamically import a module, using async syntax without extra wrappers.
Interaction with other modules
If module A uses await at the top level,
and module B imports A, then B also waits for A to finish.
// a.mjs
console.log('A start');
await new Promise(r => setTimeout(r, 1000));
console.log('A done');
// b.mjs
import './a.mjs';
console.log('B start');Output:
A start
A done
B startAdvantages
It simplifies module initialization (no async init() wrapper needed).
It removes the "import pyramid" for asynchronous dependencies.
It allows flexible asynchronous imports (await import()).
Potential downsides
Blocking the module chain: if one module "hangs" on a long await,
the whole dependency tree waits for it to finish.
It cannot be used in CommonJS (ESM only).
It can affect the application's startup time (especially in Node JS).
Example in Node.js
// package.json
{
"type": "module"
}// index.js
import fs from 'fs/promises';
const config = JSON.parse(await fs.readFile('./config.json', 'utf-8'));
console.log('Config:', config);This works because Node JS supports top-level await in ESM mode.
Summary
| Feature | Description |
|---|---|
| What it does | Lets you use await outside async functions |
| Where it works | Only in ES modules |
| What it returns | The module becomes asynchronous (returns a promise on import) |
| Use cases | Asynchronous initialization, dynamic imports |
| Risks | Blocks module loading, cannot be used in CommonJS |
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.