How to Use Top-Level await in JavaScript
A fundamental rule in traditional JavaScript was that the await keyword could only be used inside a function marked as async. Attempting to use it at the top level of a script would result in a SyntaxError. However, modern JavaScript has introduced top-level await, a powerful feature that removes this limitation within ES modules.
This guide will explain the problem that top-level await solves, how to enable it in both browsers and Node.js, and cover the classic "async IIFE" workaround for environments where top-level await is not available.
The Core Problem: await is Only Allowed in async Functions
Historically, if you tried to use await at the top level of your script, the JavaScript engine would throw an error because it was not inside an async function scope.
Problem:
// Problem: This code will throw a SyntaxError in a traditional script.
const promise = Promise.resolve('Hello, World!');
// This line would cause an error:
const value = await promise;
console.log(value);
Error Output:
Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules
This limitation made it cumbersome to handle asynchronous operations like fetching initial data or loading dependencies at the start of a script.
The Modern Solution: Top-Level await in ES Modules
Top-level await is a modern feature that allows you to use the await keyword at the top level of your code, but only when the script is treated as an ES module. This feature is incredibly useful for initializing applications, fetching configuration data, or loading other modules that have an asynchronous setup.
Solution: when your script is loaded as a module, the same code now works perfectly.
// This code works correctly in an ES module context.
const promise = Promise.resolve('Hello, World!');
const value = await promise;
console.log(value); // Output: 'Hello, World!'
How to Enable Top-Level await
To use top-level await, you must ensure your JavaScript file is being interpreted as an ES module.
In the Browser
In your HTML, add type="module" to the <script> tag.
HTML:
<!DOCTYPE html>
<html>
<head>
<title>Top-Level Await</title>
</head>
<body>
<!-- This enables top-level await in index.js -->
<script type="module" src="index.js"></script>
</body>
</html>
In Node.js
You have two main options in Node.js:
- Use the
.mjsextension: Save your file with an.mjsextension (e.g.,index.mjs). This tells Node.js to treat the file as an ES module. - Set
"type": "module"inpackage.json: Add this property to your project'spackage.jsonfile. This tells Node.js to treat all.jsfiles in that project as ES modules.
package.json:
{
"name": "my-project",
"type": "module",
"version": "1.0.0"
}
The Compatibility Fallback: The Async IIFE
If you are in an environment that does not support top-level await (e.g., an older browser or a Node.js project not configured as a module), you can use a classic workaround: an Immediately Invoked Function Expression (IIFE).
This pattern involves creating an async function and then executing it immediately, giving you an async scope to use await in.
// This works in any JavaScript environment.
const promise = Promise.resolve('Hello, World!');
(async () => {
try {
const value = await promise;
console.log(value); // Output: 'Hello, World!'
} catch (error) {
console.error('An error occurred:', error);
}
})();
This is a robust and universally compatible way to use await at the top level of a script.
Critical: Error Handling with try...catch
When using top-level await, an unhandled promise rejection can block the execution of your entire module and any other modules that depend on it. It is therefore essential to wrap your await calls in a try...catch block.
Problem:
// Problem: If this promise rejects, it can crash the module loading process.
const badPromise = Promise.reject('Something went wrong!');
// This will throw an unhandled rejection error.
const value = await badPromise;
Solution:
const badPromise = Promise.reject('Something went wrong!');
try {
const value = await badPromise;
console.log(value);
} catch (error) {
// Gracefully handle the error instead of crashing.
console.error('Caught an error:', error); // Output: Caught an error: Something went wrong!
}
Conclusion
The ability to use await outside of an async function is a powerful feature of modern JavaScript that simplifies asynchronous initialization tasks.
- Top-level
awaitis the modern and recommended best practice, but it requires your script to be loaded as an ES module. - To enable it in browsers, use
<script type="module">. In Node.js, use the.mjsextension or set"type": "module"inpackage.json. - For older environments or non-module scripts, the async IIFE pattern is a robust and universal fallback.
- Always use a
try...catchblock with top-levelawaitto handle potential errors and prevent your module from failing to load.