Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

For a long time, if you wanted to use await at the start of your program, you had to wrap everything in a giant, clunky "async main" function or an Immediately Invoked Function Expression (IIFE). You've probably seen this pattern a thousand times: (async () => { ... })();. Because of this, a lot of developers assume that Top-Level Await is just a fancy syntactic sugar for that same IIFE pattern.

The "It's Just a Shortcut for IIFEs" Myth

It's easy to think that await at the top level behaves exactly like an IIFE, but there is a fundamental difference in how the JavaScript engine handles the execution flow. Let's look at what happens when you use a traditional IIFE to fetch a configuration file before starting your app:

// config.js
export const config = { api: 'https://api.example.com' };

// main.js
import { config } from './config.js';

(async () => {
  const response = await fetch('/settings.json');
  const settings = await response.json();
  console.log('App started with settings:', settings);
})();

console.log('I run immediately!'); 

In this scenario, "I run immediately!" prints before the settings are logged. The IIFE is asynchronous; it kicks off the fetch and then moves right along to the next line of code. If other parts of your app depend on those settings, you're headed for a race condition that will be a nightmare to debug.

Unlocking Execution Order with ES Modules

Top-level await only works in ES Modules (files using import/export or scripts loaded with type="module"). Unlike the IIFE, top-level await actually pauses the execution of the module until the promise resolves.

If we rewrite that same logic using top-level await, the behavior changes completely:

// main.js (as an ES Module)
const response = await fetch('/settings.json');
const settings = await response.json();

console.log('App started with settings:', settings);
console.log('I wait for the fetch to finish first!');

Now, the second console.log won't fire until the fetch is complete. I personally find this much cleaner because it allows you to treat your module initialization as a synchronous sequence, even though it's happening asynchronously under the hood.

The Ripple Effect on the Module Graph

Here is the part that usually catches people off guard: top-level await doesn't just pause the current file; it pauses any module that imports it. This is a powerful tool, but it can be a double-edged sword.

Imagine you have a database.js module that establishes a connection using top-level await. If userController.js imports database.js, the userController will not begin executing its own code until the database connection is fully established. I've used this to ensure that a dependency is "ready for business" before any dependent code even tries to call a method on it. It eliminates the need to check if (db.isConnected) inside every single function.

Just be careful: if you have a circular dependency or a promise that never resolves at the top level, you've effectively frozen your entire application's boot process. Use it for critical initialization, not for every minor API call.




📋 Practical Task

Refactoring the Database Connection Bootloader

You have been handed a legacy initialization script that uses a messy IIFE to connect to a database. This is causing issues because the rest of the application starts running before the connection is actually established, leading to "Connection not found" errors.

Your Task: Refactor the following code to use Top-Level Await. Remove the IIFE wrapper and ensure that the app.start() method only executes after the db.connect() promise has resolved.

// Current Legacy Code
import { db } from './db-driver.js';
import { app } from './app-core.js';

(async () => {
  try {
    console.log('Connecting to DB...');
    await db.connect();
    console.log('DB Connected!');
  } catch (err) {
    console.error('Connection failed', err);
  }
})();

app.start(); // This is currently running BEFORE the DB connects!

Ensure your final solution is written as a clean ES module where the execution order is strictly guaranteed.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.