Top-Level await in ES Modules: Skip the IIFE Wrapper
The classic workaround
Before 2022 you couldn't await at the top of a file. So everyone wrote IIFEs:
(async () => {
const r = await fetch("/api/user");
const user = await r.json();
console.log(user);
})();It's just there to create an async scope. Five tokens of pure noise.
Top-level await β what every module gets
Inside any ES module (file with import / export, or <script type="module">), you can write:
const r = await fetch("/api/user");
const user = await r.json();
console.log(user);The module load itself becomes async. Anything that imports this module waits until your top-level awaits finish. That's exactly what you want for "initialize the config before anyone uses it."
The cost
Top-level await delays the module's "done loading" event. If three modules each have a 500ms top-level await, the user sees nothing for 1.5s. So:
- Use it for initialization that genuinely can't happen later (config from a fetch, WASM module compile, decryption key).
- Avoid it for "would be nice to prefetch" stuff β do that lazily on first call instead.
What it doesn't do
Top-level await doesn't make sibling modules wait on each other unless they directly import each other. ES module loading is still parallel where it can be β top-level await just adds a "ready" gate on the importing side.
Sign up to start coding
Theory is open to everyone. The interactive editor, live preview, and check are unlocked with a 7-day free trial β card required, cancel anytime.
Sign up β free trial βFirst 10 lessons in each track are free. No card needed for those.