The Problem
You probably hit a wall while trying to fetch data or initialize a database connection directly at the root of your file. Instead of a smooth execution, TypeScript blocks you with a compiler error because it doesn't recognize your file as a modern module.
Consider this common scenario:
// index.ts
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
By default, TypeScript often targets CommonJS. This legacy system relies on require() and is strictly synchronous. Since top-level await is a feature exclusive to ECMAScript Modules (ESM), the compiler throws an error to prevent your code from breaking at runtime.
TL;DR: The Quick Fix
Open tsconfig.json and update your settings to use modern standards. These three lines usually solve 90% of cases:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "nodenext"
}
}
If you are building for Node.js, you must also add "type": "module" to your package.json.
Detailed Fix Approaches
1. Modernize your tsconfig.json
The error message is actually quite helpful. It lists the specific module systems that support this feature. To fix this properly, ensure your configuration tells TypeScript that the environment is modern enough to handle ESM.
{
"compilerOptions": {
/* ES2022 is the minimum target for native top-level await support */
"target": "es2022",
/* Use 'nodenext' if you are on Node.js 16 or newer */
"module": "nodenext",
"moduleResolution": "nodenext",
/* Includes modern API definitions in your IDE */
"lib": ["esnext", "dom"]
}
}
Using nodenext is the safest bet for modern backend projects. It forces TypeScript to follow the same rules as Node.js, such as requiring file extensions in your import statements.
2. Enable ESM in package.json
Updating TypeScript isn't always enough. If you run your compiled .js files in Node.js, the runtime needs to know it should treat them as modules rather than old-school scripts. Add the type field to your package.json file:
{
"name": "my-app",
"version": "1.0.0",
"type": "module"
}
Without this, Node.js will throw a SyntaxError: Unexpected reserved word when it hits the await keyword, even if the TypeScript compiler was happy.
3. The IIFE Workaround (For Legacy Projects)
Sometimes you can't switch to ESM. You might be stuck with a legacy codebase or dependencies that crash under modern module rules. In these cases, wrap your logic in an Immediately Invoked Function Expression (IIFE).
(async () => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (err) {
console.error("Initialization failed", err);
}
})();
This puts the await inside an async function block. This satisfies the compiler while still letting your code run immediately upon startup.
Why this happens
JavaScript was originally designed to load files one after another in a simple sequence. When async/await arrived in ES2017, it was restricted to function bodies to keep things predictable. As the ecosystem shifted toward ECMAScript Modules, the specification evolved. Top-level await was officially added in ES2022. However, CommonJS—the system Node.js used for over a decade—is fundamentally synchronous. It cannot pause execution to wait for a Promise at the top level of a file.
How to verify the fix
Don't just trust the lack of red squiggly lines. Follow these steps to ensure everything is configured correctly:
- Refresh the TS Server: In VS Code, open the Command Palette (Cmd/Ctrl + Shift + P) and run "TypeScript: Restart TS server".
- Check the Build: Run
npx tsc. The command should finish without outputting any errors to your terminal. - Inspect the Output: Open your
dist/index.jsfile. You should see theawaitkeyword exactly where you wrote it, rather than a messy pile of transpiled__awaiterhelpers. - Test the Runtime: Run
node dist/index.js. If your data logs to the console, you're good to go.
Common Pitfalls
- Outdated Node.js: Top-level await requires Node.js 14.8.0 or higher. If you're on an older LTS version like Node 12, none of these fixes will work.
- The 'target' trap: If your
targetis set toes5, TypeScript will try to turn your code into 20-year-old JavaScript. This will cause errors because ES5 has no concept of Promises.

