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).

await allows you to untangle Promises-based code and make it 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 this wrapping 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 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 diving right in, 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.

// 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 ES module in these situations.

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

Await, await, await...

This functionality is beautiful! I'll probably stick to the .mjs file extension for 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, I send out a weekly newsletter. 👇

Related Topics

Related Articles