Top Level Await is literally awesome. It's the GOAT!!(Greatest of All Time, in case you couldn't guess 😉)
The Dark Times...
There was an era, where if you tried to pull a stunt like this 👇 at the top level(i.e. not in any async
function),
const data = await fetch(URL);
JS would scream at you 👉 SyntaxError: await is only valid in async function
It was super frustrating. But what could you do then?
The Hack
Wrap it in IIFE
IIFE: Immediately Invoked Function expressions. Flavio Copes has a really good article about it.
(async () => {
const data = await fetch(URL);
})();
Not really a hack as far as official spec is concerned, but to the code author, it definitely feels like one.
Just look at the code. So many brackets, so much boilerplate. The last line with })();
makes me nauseous even after 5 years of JS development. So many weird brackets!!
But wait, it gets even better 😑
(async () => {
const response = await fetch(URL);
const jsonData = await response.json();
const finalData = await processJsonData(jsonData);
if (finalData.propA.propB === 'who-cares') {
// Do stuff
}
})();
This code gets messier. And that code above is still very clean. Wait till you try to create your version of MacOS Desktop for Web (Shameless Plug! I'm working on it 😁 macos.now.sh). It's gonna get outright ugly, and you don't want ugly code. Nobody wants ugly code.
A New Hope
If you're wondering why I'm using Star Wars related words a lot, Mandalorian episode 16 dropped a few days ago, and literally, ************** appeared 😭. I'm still shaking from how good that episode was.
In comes Top Level await, slashing droids with his lightsaber, taking the pains of IIFE hacks away.
Using it is as simple as the first code snippet on top:
const data = await fetch(URL);
And it will work perfectly.
And that second snippet, see this 👇
const response = await fetch(URL);
const jsonData = await response.json();
const finalData = await processJsonData(jsonData);
if (finalData.propA.propB === 'who-cares') {
// Do stuff
}
Perfection 👌.
But, there are certain requirements to use it.
Requirements
It can be used only in ES Modules.
That is, in scripts that are marked as modules in your HTML or in your package.json in Node
Browser
In browser, JS alone is nothing. It needs to be linked to by the HTML file.
In your index.html
:
<script type="module" src="index.js" />
type="module"
is necessary for it to be interpreted as an ES Module
NodeJS
You need to have minimum of Node 13.9.0 for this feature to work. The current LTS is v14.15, and I recommend most users to always choose the LTS version. If you're reading this in 2025, and the LTS is v24, go for it, not 14.15. (I hope Node survives that long, what with Deno and Elsa being there now 😅)
Note: I'm aware that you could use ES Modules long before 13.9.0 in NodeJS, but you had to pass the flag
--experimental-module
, as innode index.js --experimental-module
, and these modules were highly experimental and unstable and subject to change then, so I didn't even bother with them.
These below are some steps to get ES Modules in Node working. Note that these aren't the only methods for that. There are total of 2 or 3 right now, but I will explore the most common one only.
Step 0
Have npm installed. If you already have node installed, you need not worry, you already have it.
Check Node version:
node -v
Check npm version:
npm -v
npm should be higher than 6.14.8
at this point of time.
But the Linux users might have some issues, as running sudo apt install nodejs
downloads a super-old version of Node, and even without npm, that is (The Blasphemy 😤).
In that case i recommend you to install nodeJS and npm using this very good article.
But beware, your problems won't be over because of the permissions issues. I recommend you to install nvm
(Nope I didn't misspell npm
), which will take care of all these problems for you. Read how to install nvm.
After you have installed nvm, Run nvm install --lts
to install the latest LTS version.
It's slightly longer method, but much less painful, both in short and long term
Step 1
Create package.json
Most Node projects will already have the package.json
ready, but in case you don't, make one. It's as simple as typing this command:
npm init -y
This should output a file of this format. Values may be different, but format stays the same:
{
"name": "snippets",
"version": "1.0.0",
"description": "",
"main": "promise-arr.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Step 2
Add "type": module"
in the JSON file. Like this:
{
"name": "snippets",
"version": "1.0.0",
"description": "",
"main": "promise-arr.js",
+ "type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
And you're good to go!
Use Cases
Here are some common use cases for top level await:
Note: These use cases are quite simple, and will be most probably composed inside functions, where you can already use
async
. The most use of top level await would be to consume these higher order functions in the main code.
Timer
Whenever I jump onto any project, I carry some utility functions with me. One such utility functions is the simpler alternative to using the ugly setTimeout
, and it gets rids of some weird use cases that comes with setTimeout
. It's the waitFor
utility function:
/**
* @param {number} time Time to wait for in milliseconds
*/
function waitFor(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
I use it simply as:
doTask1();
await waitFor(200);
doTask2();
await waitFor(200);
doTask3();
I can use it directly in modules with top level await like this:
import { waitFor } from '../utils.js';
console.log(1);
// Wait for 200ms
await waitFor(200);
console.log(2);
I have written a blog post about this utility function too. Check it out here
Dependency fallbacks
Let's say you're using your own remote server for importing Modules directly. You have come up with some superb optimization algorithms to make those imports from remote server even faster than locally bundled imports, and are willing to rely more on that server.
But it's your server. You have to maintain it. 24/7!! What if it goes down? It would be a huge bummer then, wouldn't it?
So you come with a clever solution: Import from your own server, but if it fails, import from unpkg. Seems smart. So you write this code:
try {
import jquery from 'https://my-server.com/api/jquery.js';
} catch {
import jquery from 'https://unpkg.com/jquery@3.3.1/dist/jquery.js';
}
const $ = jquery.default;
Ahem! One catch here. This code is invalid. You can't use import package from "somewhere"
inside any block. It has to be used in the top level only (This seems like the inverse problem of Top Level Await, isn't it 🤔).
Luckily, we can use the dynamic import
statement, which can be used anywhere.
So our new code becomes.
let jquery;
try {
jquery = await import('https://my-server.com/api/jquery.js');
} catch {
jquery = await import('https://unpkg.com/jquery@3.3.1/dist/jquery.js');
}
const $ = jquery.default;
That's it! See, we used await without any async function wrapping it. It's on the top-most level. The code will wait for the import
in the try
block to resolve, then if it fails, will go fetch from unpkg
, and waiting while it happens, but not stopping the execution altogether.
Internationalization (i18n)
Had to search Google for the spelling of this word 😅
Say you have some JS files in which you're storing common strings for different languages.
Now you wish to access those strings right on top, without any other wrapper or function. You can do it simply like this:
const strings = await import(`../i18n/${navigator.language}`);
paragraph.innerHTML = strings.paraGraph;
See how simple it is?
And most bundlers like Webpack/Rollup will recognize that you're trying to fetch some files from the ../i18n
folder, so they'll just create separate, individual bundles of the files in the ../i18n
folder, and you can import the right, optimized bundle.
This has traditionally been done with JSON files and
fetch
ing them, but these dynamic imports open up new possibilities
Resource initialization
Let's consider a backend-related example for this. Say you have a Database implementation with lots of initialization code. Well, you'd need to initialize your database someway, and most of these databases take some amount of time, so they're always callback or promise based. Let's assume, in our case, the database instance is promise based (You can convert callback based functions to promises in NodeJS too, using require('util').promisify
).
So you initialize it:
import { dbInstance } from '../db';
const connection = await dbInstance();
// Now we can simply pass the database instance to the function below
const userData = await getUserData(connection);
See how simple and idiomatic it is?
Conclusion
Top Level Await is an awesome addition to JavaScript, and is here to stay. Heck, even Darth Vader agrees
Signing off! 😁
Top comments (16)
If you want to limit parenthesis and brackets :
A New Hope
I disagree, it was just fanservice 😅 but good crossover with Terminator 😂
Cap lifting the hammer was also fanservice, but it was so freaking great!!! Same in this one.
Indeed but cap was a real actor (until he aged 😅) !
(Just trolling 😘)
What crossover with Terminator? The Dark troopers?
Yup
How did you highlight the text in the post?
Use mark tag
What's the "mark" tag? And why can't I use color tags with the "color"/"style" properties?
w3schools.com/tags/tag_mark.asp
As for yur other qn, dunno. I just write markdown for my blog site's blog engine, then copy paste it on dev to
That didn't explain how you colored them.
Do I add the color property to it? Or....?
I didn't color anything myself. You talking about the yellow backgroumd?
Yes,
I used the mark tag. That's how
facepalm
Great post! I love top level await, just wish cjs could use it, though I've been using typescript more often as of recently.
Typescript is our ************* in mandalorian last episode.
And there's your spoiler free response