Loading an ES6 module in a Node.js CommonJS module

; Date: Thu Apr 22 2021

Tags: Node.JS »»»» JavaScript

ES6 modules are a nifty new way to write modules. They're rather similar to the CommonJS module format traditionally used by Node.js, but there are significant differences. When ES6 module support was first added to Node.js, it was an experimental feature with significant limitations. One limitation was whether you could use ES6 modules from CJS modules, and there was a 3rd party package to fill that gap. Today, ES6 modules are a first class feature in Node.js, and ES6 modules can be used from CJS without any add-on support.

ECMAScript v6 brought in a lot of game changing improvements to the JavaScript landscape, and the ECMAScript team have been rolling out new features every year. One of the advances, ES6 modules, gives us a common module format to use between both browser and server-side JavaScript. This module format has a lot of capabilities, way more than the CommonJS module format traditionally used on Node.js. More importantly, coding with ES6 modules means you're able to use the same tools and techniques on both.

The downside of using ES6 modules on Node.js is the long period of time in which CommonJS modules were the only choice. As a result CommonJS has a lot of inertia in the form of lots of existing code. Unfortunately the interoperability between ES6 and CJS modules on Node.js has a couple small wrinkles.

To help the Node.js platform distinguish between CJS and ES6 modules, two new file extensions are being used:

  • The traditional .js file extension by default identifies CommonJS modules, but Node.js can be configured to recognize .js as ES6 modules instead
  • The .mjs file extension is always recognized as an ES6 module
  • The .cjs file extension is always recognized as a CJS module

The methods for loading modules are:

  • An ES6 Module can load other ES6 modules using import
  • An ES6 Module can load CommonJS using import
  • A CommonJS module can load other CommonJS modules using require
  • A CommonJS module previously COULD NOT load an ES6 Module, but today it can load an ES6 module using the import() function

At first blush you might think either type of module can load the other type. But there are differences between require and import() that require careful attention.

Module loading review

To review CommonJS ... a module is loaded as so:

const TheModule = require('module-specifier');

There are well-known rules for the module-specifier in the Node.js documentation. For exammple, ./foo loads a module in the current directory, whereas foogrep loads a module from the node_modules directory.

ES6 modules load other ES6 modules as so:

import TheModule from `es6-module-specifier`;

That's the basic mode, there are several alternatives available.

And an ES6 module loads CommonJS modules as so:

import TheModule from `module-specifier`;

In both cases the module-specifier uses the same rules for locating the module as Node.js supports in the require function.

Notice we haven't discussed how to load an ES6 module from CJS.

Loading an ES6 module from a Node.js CommonJS (CJS) module

Suppose you have an ES6 module named hello.mjs containing:

export async function hello() {
    return "Hello, world!";
}

export function func1() {
    return "Synchronous ES6 function";
}

Pretend we have an API with both synchronous and asynchronous functions.

Now, consider a script named app.mjs containing:

import hello from "./hello.mjs";

console.log(await hello.hello());

console.log(hello.func1());

This script is an ES6 module. It loads the ES6 module, and invokes both methods. Starting with Node.js 14.8 we are able to use await in top-level code of an ES6 module.

Now consider the equivalent script, written as a CJS module:

const _hello = import('./hello.mjs');

_hello
.then(condit => {
    hello.hello()
    .then(msg => { console.log(msg); })
    .catch(err => { console.error(err); });
    
    console.log(hello.func1());
})
.catch(err => { console.error(err); });

In top-level code of CJS modules we cannot use the await keyboard, forcing us to use .then/.catch as shown here.

The import() function returns a Promise, because it loads the module asynchronously. The CommonJS require statement loads modules synchronously, meaning the module is fully loaded when require finishes, but import() is asynchronous. Instead it returns a Promise, and when the Promise becomes resolved the module is fully loaded. Initially, however, the Promise is in the pending state.

That means to use the module we must accommodate for this Promise.

Another way to implement this is:

const _hello = import('./hello.mjs');

async function runApp() {
    console.log(await (await _hello).hello());
    console.log((await _hello).func1());
}

runApp()
.catch(err => { console.log(err); });

The advent of async/await functions has been a godsend in simplifying asynchronous code. In both cases we await the Promise in _hello to get the module reference. We can then call the functions. For the hello function invocation, the second await is required because hello is itself an async function.

Summary

This article is updated from one written in 2018 when ES6 modules were new to Node.js. Since then ES6 module support has improved greatly.

To learn more about interoperability between ES6 and CJS modules on Node.js, read this companion article:

Complete guide to using ES6 modules to create Node.js packages that are easily usable from CJS modules It is very tempting to write all JavaScript code on Node.js with ES6 modules. But that creates a problem of interoperability with the large number of existing CommonJS (CJS) modules. Namely, it is difficult to use ES6 modules from a CJS module. Node.js makes it easy to use both CJS and ES6 modules from an ES6 module, and CJS modules can easily use CJS modules, but a CJS module using an ES6 module is painful. At the same time ES6 modules offer enough attractive benefits to warrant rewriting our code to switch from CJS to ES6 modules. This means we need a strategy for easily using Node.js ES6 modules from CJS modules.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron

(Sponsored)