Skip to main content

Node.js Script writers: Top-level async/await now available

 Async/await functions are a god-send to JavaScript programmers, simplifying writing asynchronous code. It turns difficult-to-write-and-debug pyramids of doom into clean clear code where the results and errors land in the natural place. It’s been wonderful except for one thing, we could not use the await keyword in top-level code in Node.js. But, with Node.js 14.8 that’s fixed.

Image for post

Writing scripts probably involves reading or writing files, retrieving external data, to work on that data and produce a result, which might be written back to an external file or database. In Node.js, that requires asynchronous code. And in a script, where the code is at the top level of a JavaScript file, that prevented us from using the await keyword.

Consider:

const fs = require('fs').promises;  const txt = await fs.readFile('test.txt', 'utf-8');
console.log(txt);

A script like we just described might do some processing on the input file, then write its output somewhere else. But let’s focus on the asynchronous act of reading the data file.

This is a traditional CommonJS style of Node.js program. The programmer said to themselves, let’s use await because that is such a useful keyword. The programmer was even careful to use require('fs').promises to get the promisified fs package. But, running the script they see this:

$ nvm use 14.7
Now using node v14.7.0 (npm v6.14.7)
$ node m1.js
/Users/David/nodejs/top-level-async-await/m1.js:8
const txt = await fs.readFile('test.txt', 'utf-8');
^^^^^
SyntaxError: await is only valid in async function
at wrapSafe (internal/modules/cjs/loader.js:1172:16)
at Module._compile (internal/modules/cjs/loader.js:1220:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1277:10)
at Module.load (internal/modules/cjs/loader.js:1105:32)
at Function.Module._load (internal/modules/cjs/loader.js:967:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47

Throughout this post I’ll use nvm to manage the Node.js version being used. The importance of choosing the 14.7 release will become clear later.

That’s what we’ve lived with for the last couple years in Node.js. We could use the await keyword inside a function marked with the async keyword, but we could not use it in top-level code. In other words, that script would have to be written as:

const fs = require('fs').promises;  
(async () => {
const txt = await fs.readFile('test.txt', 'utf-8');
console.log(txt);
})();

This wraps the asynchronous await invocation inside a light-weight async function that is immediately invoked. This is a simple technique that lets us write async/await code almost in the top level of the Node.js script. IMPORTANT NOTE: this simple script is missing some important error handling.

What we really wanted was to write code like the first snippet in this post. This was the closest we could get, until now.

Top-level async/await in Node.js 14.8

Node.js 14.8 was released today, and contains a very important advancement. In an ES6 module we can now use the await keyword in top-level code, without any flags.

That means in a file with the .mjs extension we can write this:

import { promises as fs } from 'fs';  const txt = await fs.readFile('test.txt', 'utf-8');
console.log(txt);

This import statement is how we access the promisified fs module in an ES6 module. Otherwise this is the same as the example at the top.

And we can run it like this:

$ nvm use 14.8 
Now using node v14.8.0 (npm v6.14.7)
$ node m1.mjs
Hello, world!

This works as we want it to work, await executing perfectly in the top level.

This is how the same example worked with Node.js 14.7

$ cat test.txt  
Hello, world!
$ nvm use 14.7
Now using node v14.7.0 (npm v6.14.7)
$ node m1.mjs
file:///Volumes/Extra/nodejs/top-level-async-await/m1.mjs:6
const txt = await fs.readFile('test.txt', 'utf-8');
^^^^^
SyntaxError: Unexpected reserved word
at Loader.moduleStrategy (internal/modules/esm/translators.js:122:18)
at async link (internal/modules/esm/module_job.js:42:21)

That’s the significance of the change in Node.js 14.8. We can now use the await keyword in top level Node.js code, giving us natural looking async scripts. We no longer have to face this unpleasant error.

On a final node, this feature only works when Node.js is running an ES6 module. If it is executing a CommonJS module, like the first example:

$ node --version
v14.8.0
$ node m1.js
/Volumes/Extra/nodejs/top-level-async-await/m1.js:8
const txt = await fs.readFile('test.txt', 'utf-8');
^^^^^
SyntaxError: await is only valid in async function
at wrapSafe (internal/modules/cjs/loader.js:1167:16)
at Module._compile (internal/modules/cjs/loader.js:1215:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1272:10)
at Module.load (internal/modules/cjs/loader.js:1100:32)
at Function.Module._load (internal/modules/cjs/loader.js:962:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
at internal/main/run_main_module.js:17:47

This is the same CommonJS example shown at the top, but executed this time with Node.js 14.8. It’s the same error in both cases.

That was nice, we can now use async code in a Node.js script. But let’s try a more comprehensive example rather than this artificial simple thing.

Async download and processing of images in a Node.js script

The node-fetch package brings the fetch function to Node.js, making it an excellent way to download a file from somewhere. The download is processed as an async invocation, of course. The sharp package is an excellent tool for manipulating images. Let's use the two together to download and process an image.

On the Node.js website (at https://nodejs.org/en/about/resources/) there is available SVG versions of the standard Node.js logo. It’s important to properly use the correct trademarked image. Let’s write a little script to download the SVG, resize it to 300 pixels, make it grayscale, and write it to a JPG.

First, in a directory with a package.json:

$ npm install sharp node-fetch --save

This installs the required packages. If you prefer Yarn, use that.

Then we write a file, node-logo.mjs, containing:

import { default as fetch } from 'node-fetch'; 
import { default as sharp } from 'sharp';
import { default as fs } from 'fs';
(await fetch('https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg'))
.body.pipe(
sharp().resize(300).grayscale()
.toFormat('jpeg')
)
.pipe(fs.createWriteStream('node-logo.jpg'));

The fetch function returns a Promise, and we use await to handle its success or failure. I do not understand why the node-fetch documentation tells us to use a .then handler when we can just use await. Used this way it produces a ReadStream that can be piped somewhere.

In the middle portion of the pipeline we have an image processor implemented with Sharp. We simply tell it to resize the image, make it grayscale, then convert to JPG format.

The final portion of the pipeline uses a WriteStream to send the data to the named file.

With Node.js 14.7 we had to write this inside an async function like was shown earlier. With Node.js 14.8 it simply runs like so:

$ node node-logo.mjs

And it produces this:

Image for post

Summary

This advancement will unlock a new ease for writing simple scripts in Node.js. There are close to a zillion different one-off tasks we might want to do with Node.js. For example, I was exploring, just a few days ago, that exact image download/resize/etc pipeline for use with my static website generator (AkashaCMS). The example I wrote just last week had to use the async wrapper function, but today that’s no longer required.

This should eliminate one of the Node.js adoption hurdles. Instead of having to turn to Python or other languages for simple scripts like this, we can stay with Node.js.

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...