Skip to content

All you need to know to move from CommonJS to ECMAScript Modules (ESM) in Node.js

One of the most revolutionary features introduced as part of ECMAScript 2015 specification is modules (ESM). The first browser implementation landed in April 2017 in Safari 10.1. I published a “Native ECMAScript modules in the browser” about this historical moment. A few months later, in September 2017, Node v8.5.0 landed with experimental support for ESM.

This feature went through lots of iterations during its experimental phase. A few years later, in April 2020, Node v14.0.0 landed without experimental modules warning. Even though it was still experimental, it felt like the right timing to adopt ESM for some toy projects and insignificant clients work. Eventually, Node v15.3.0 arrived and marked modules implementation as stable.

ECMAScript modules marked as stable in version 15.3

That’s enough of history, so let’s get our hands dirty and dive into the ECMAScript modules in Node.js. We have a lot to cover, so let’s jump into it!

Enabling ECMAScript modules (ESM) in Node.js #

To preserve backward compatibility, Node.js treats JavaScript code as CommonJS by default. To enable ESM, we have three options.

  • use .mjs extension (colloquially known as Michel’s Jackson’s modules)
  • add "type": "module" to package.json file
  • use --input-type=module flag for STDIN or strings passed to --eval argument

Syntax #

ECMAScript module introduced a new syntax. Have a look at the example written in CommonJS and equivalent using ESM.

// util.js
module.exports.logger = (msg) => console.log(`๐Ÿ‘Œ ${msg}`);

// index.js
const { logger } = require("./util");

logger("CommonJS");
// ๐Ÿ‘Œ CommonJS
// util.js
const logger = (msg) => console.log(`๐Ÿ‘Œ ${msg}`);

export { logger };

// index.js
import { logger } from "./util.js";

logger("ECMAScript modules");
// ๐Ÿ‘Œ ECMAScript modules

There’s a lot more to explore in terms of syntax but I will leave that with you as the Node.js closely conforms to official ESCMAScript modules syntax. Pay attention to the file extension (.js or .mjs) needed to correctly resolve relative or absolute specifiers. This rule also applies to directory indexes compared to CommonJS (e.g. ./routes/index.js).

Strict by default #

There is no need for use strict on the top of your program to prevent the runtime from running in sloppy mode. ECMAScript modules run in strict mode by default.

Browser compatibility #

Because ESM implementation in Node.js and the browser conforms to the exact specification, we can share code between server and client runtime. In my opinion, the unified syntax is one of the most appealing benefits of using ESM.

<srcipt type="module" src="./index.js"> </srcipt>

“Get Ready For ESM” by Sindre Sorhus goes in-depth about other benefits of unified syntax and encourages package creators to make a move to ESM. I can’t agree more with this fantastic dude!

ESM is missing some references #

ECMAScript modules enabled runtime is missing some commonly used in CommonJS references:

  • exports
  • module
  • __filename
  • __dirname
  • require
console.log(exports);
// ReferenceError: exports is not defined

console.log(module);
// ReferenceError: module is not defined

console.log(__filename);
// ReferenceError: __filename is not defined

console.log(__dirname);
// ReferenceError: __dirname is not defined

console.log(require);
// ReferenceError: require is not defined

As we discussed above, when using ESM, we don’t need access to exports and module anymore. We can recreate the remaining references that are missing.

// Recreate missing reference to __filename and __dirname
import { fileURLToPath } from "url";
import { dirname } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__dirname);
console.log(__filename);
// Recreate missing reference to require
import { createRequire } from "module";

const require = createRequire(import.meta.url);

Behavior of this keyword #

It’s worth mentioning that the behaviour of this keyword differs in the global scope. In ESM, this is undefined, however in CommonJS, this keyword points to exports. Worth remembering this subtle difference.

// this keyword in ESM
console.log(this);
// undefined
// this keyword in CommonJS
console.log(this === module.exports);
// true

From dynamically parsed CommonJS to statically parsed ESM #

CommonJS modules are parsed dynamically during the execution phase. This functionality allows calling the require function inside the block (i.e. inside if statement) as the dependency graph is explored during the program execution.

ECMAScript modules are much more sophisticated โ€” before running, the interpreter will build a dependency graph and then execute the program. Predefined dependencies graph allows the engine to perform optimizations such a tree shaking (dead code elimination) and more.

ESM with top-level await support #

Node.js in version 14 enabled support for top-level await. This changes dependency graph rules a little and makes a module act like a big async function. Example time!

import { promises as fs } from "fs";

// Look ma, no async function wrapper!
console.log(JSON.parse(await fs.readFile("./package.json")).type);
// module

Importing JSON #

Importing JSON is a frequently used feature in CommonJS. Unfortunately, doing that while using ESM will throw an error. As recommended above, we can overcome this limitation by recreating require function.

import data from "./data.json";
// TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".json"
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const data = require("./data.json");

console.log(data);
// {"that": "works"}

The best time to embrace ESM is now #

I hope this article helped you to understand the differences between CommonJS and ECMAScript modules in Node.js. I’m looking forward to the times where we won’t have to care about these differences anymore. The whole ecosystem will work accordingly to the ECMAScript specification regardless of the runtime (either client or server). If you haven’t already, I would highly recommend you jump on the ESM camp now and contribute to the consistent and unified JavaScript ecosystem.

I enjoyed writing this down for you! If you found it helpful, “hit that like button and don’t forget to subscribeโ€ฆ”. Nah, I’m just joking. Share it with your friend โ€” this will mean the world to me! Thanks, and until next time, stay curious ๐Ÿ‘‹

What'ya think?

๐Ÿ‘† you can use Markdown here

Your comment is awaiting moderation. Thanks!