JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
103: Top-Level Await
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.
There are no comments for now.