Google Sheets iconSwift icon
Published at
Updated at
Reading time
2min

This post is part of my Today I learned series in which I share all my learnings regarding web development.

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.

But before getting into it, 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.

If you like these quick tips, check out my weekly newsletter. 👇

Related Topics

Related Articles