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: