CJS and ESM — can’t we all just get along?

Dotan Reis
3 min readFeb 3, 2021

A name can say a lot about something. For example, CJS and ESM.

To read “CJS”, you read every letter, understand its reference, and go on. C(ommon)J(ava)S(cript).

“ESM” is a bit different. When I first looked it up I found that “ESM = ES module”. Gee, thanks. What’s ES? well apparently it’s a short for ECMAScript, which is some prototype language JS is based on. I still wanted to understand the acronym so I looked into why ECMAScript is called this way, turns out it was created by ECMA — European Computer Manufacturers Association. So finally, ESM = European Script Module. Cool. Now we can begin.

Photo by Stillness InMotion on Unsplash

To be practical, we’re really talking about two ways to write javascript modules.

CJS looks like this:

//importing
const doSomething = require('./doSomething.js');
//exporting
module.exports = function doSomething(n) {
// do something
}

And ESM looks like this

import {foo, bar} from './myLib';...export { ... }

We all know and love this because all our JS code is written in CJS, and our TS code in ESM. (BTW, our TS compiler creates CJS files in the end, but I think it can do either one)

Are CJS and ESM just different syntaxes for the same thing?

No. The “import” process is actually very different from the “require” process.

If we have a CJS file like this:

console.log(1);const { b } = require('./b');console.log(2);

Node will execute console.log(1), then get, parse and run b, then execute console.log(2). If b has dependencies they will be executed in between.

In a ESM file:

console.log(1);import { b } from './b';console.log(2);

Node will first get and parse b, then execute b, and only then execute this file.

Basically ESM creates a graph of imports, and then executes them in reverse order — from those without dependencies to the main file which is executed last. All modules are cached so they won’t need to be executed twice. (Remember how we parsed the name ESM?)

ESM modules can split the “require” process into steps. They can load the files, parse them, instantiate them and evaluate them separately. This allows them to download all the files before executing anything.[2] (Which is much more efficient when you’re getting files from the internet, which can take some time)

ESM also has some implications on the code.

  • ESM modules automatically add ‘use strict
  • They allow top level “await”
  • The exported values share a memory space with the imported values. In CJS the imported values are copies of the exported ones.
  • You can’t use variables in import statements, because the variables are not yet instantiated. (Unless you use dynamic imports)

So… can they just get along?

It’s complicated. The top-level await issue alone means that to “require” an ESM module, you’d need to “await” the require somehow, which has weird implications on how CJS works.

You can’t “import” named values from a CJS file, because ESM expects the named exports to exist in the parsing stage, and in CJS they are only created when the script is executed. (See details in [1])

In short, there are ways to make CJS and ESM work together (see [1] for tips), but generally they are like avocados and pasta — just better apart.

References:

  1. https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1 (Focuses on the differences between CJS and ESM, how to reconcile them)
  2. https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/ (A deep dive into what are modules and how ESM works under the hood)
  3. https://irian.to/blogs/what-are-cjs-amd-umd-and-esm-in-javascript/ (Short summary of the differences between these and other module types)

--

--

Dotan Reis

Software developer @ riseup. MA student @ The Cohn Institute in Tel Aviv University