Published at
Updated at
Reading time
2min
This post is part of my Today I learned series in which I share all my web development learnings.

Node.js is a beautiful tool to write quick utility scripts. I use it in many of my build processes. Renaming files, downloading data, image processing โ€“ Node.js scripts handle many tasks in my projects.

There has been one tiny annoyance, though. When dealing with asynchronous functionality such as making network requests, there was no top-level await support in Node.js (yet).

The await keyword allows you to untangle Promises-based code, avoid chained then calls and make source code more readable.

// promise-based code
Promise.resolve('hello world').then((asyncMsg) => {
  console.log(msg);
});

// async/await code
const asyncMsg = await Promise.resolve('hello world');
console.log(msg);

Unfortunately, you could not use the await keyword without wrapping it in an async function.

// use an async IIFE
(async () => {
  const asyncMsg = Promise.resolve('hello world');
  console.log(asyncMsg);
})();

// use an async main function
async function main() {
  const asyncMsg = Promise.resolve('hello world');
  console.log(asyncMsg);
}

main();

And while wrapping code in an async function is not terrible, its whole purpose is to enable the await keyword. Is there a better way? Can we avoid these async wrappers in Node.js code? Top-level await is now coming to the rescue!

top-level await is available "unflagged" in Node.js since v14.8

Starting with Node.js v14.8, top-level await is available (without the use of the --harmony-top-level-await command line flag).

There's one catch: top-level await is only available in ES modules. There are three ways to make a Node.js script an EcmaScript module.

Be aware that if you're enabling ES modules in Node.js, you have to change all require and module statements with their import and export counterparts.

Use the mjs file extension

Use the .mjs file extension and call it a day! ๐ŸŽ‰

// File: index.mjs
//
// Command line usage: node index.mjs

const asyncMsg = await Promise.resolve('WORKS!');
console.log(asyncMsg); // "WORKS!"

Make the whole package a module

If you're developing a package you can also define the type property in your package.json to declare that it's based on ECMAscript modules.

// File: index.js
//       (near package.json including { "type": "module" })
//
// Command line usage: node index.js

const asyncMsg = await Promise.resolve('WORKS!');
console.log(asyncMsg); // "WORKS!"

Define input-type when evaluating string input

Sometimes you might need to pipe code into the Node.js binary or use the eval flag. Use the input-type flag to specify that the passed string value is an ECMAscript module.

node --input-type=module \ 
  --eval="const asyncMsg = await Promise.resolve('WORKS!'); console.log(asyncMsg);"

Await, await, await...

It's beautiful that top-level await finally entered Node.js! I'll probably stick to the .mjs file extension to use it in my scripts. Renaming a script file from js to mjs is quickly done and is not introducing significant changes.

But wait! Before going all in with top-level await, you might want to check how Node handles unsettled top-level promises.

If you enjoyed this article...

Join 5.2k readers and learn something new every week with Web Weekly.

Web Weekly โ€” Your friendly Web Dev newsletter
Stefan standing in the park in front of a green background

About Stefan Judis

Frontend nerd with over ten years of experience, freelance dev, "Today I Learned" blogger, conference speaker, and Open Source maintainer.

Related Topics

Related Articles