Skip to main content

What is "top-level await"?

What it was like before

Before top-level await, await could only be used inside an async function:

javascript
// 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:

javascript
// 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 .mjs extension, or
  • in .js, if package.json has "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:

  1. The module becomes asynchronous.
  2. Its execution pauses until the await resolves.
  3. Other modules that import this module also wait for it to finish executing (through the dependency chain).

Example 1 - importing with a wait

javascript
// user.mjs export const user = await fetch('/api/user').then(r => r.json());
javascript
// 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

javascript
// 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

javascript
// 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.

javascript
// 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:

javascript
A start A done B start

Advantages

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

javascript
// package.json { "type": "module" }
javascript
// 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

FeatureDescription
What it doesLets you use await outside async functions
Where it worksOnly in ES modules
What it returnsThe module becomes asynchronous (returns a promise on import)
Use casesAsynchronous initialization, dynamic imports
RisksBlocks module loading, cannot be used in CommonJS

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.