completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

CommonJS & ES module interop

Learn how current Node.js chooses CommonJS or ES modules, how require and module.exports interact with import and export, why require(esm) has caveats, and how to avoid dual package hazards.

By the end you can
  • 01
    Predict the loaderUse the file ending, the type in package.json, and flags to tell whether Node runs a file as CommonJS or as an ES module.
  • 02
    Load one kind from the otherExplain what a default import of CommonJS gives you, when a named import works, and what require() gives back for an ES module.
  • 03
    Avoid package trapsSpot when a dual package splits its saved data into two copies, and choose a safer design.

Two kinds of module

A module is a JavaScript file that shares some of its values with other files. Node.js has two kinds of module:

  • CommonJS is Node's older kind. A file asks for another file with require("./tool"). It shares values by putting them on module.exports.
  • ES modules (ESM for short) are the standard kind. Browsers use them too. A file shares values with export and asks for them with import.

The two kinds load files in different ways:

  • With CommonJS, require() runs the other file right away, on that line. Then it gives back that file's module.exports.
  • With ES modules, Node first reads every file and finds all the import and export lines. It connects them. Only then does it run the code in the files.

You learned why modules exist in Why modules?, and you practiced import & export. This lesson is for Node projects that use both kinds together. Browsers can't run CommonJS files on their own. So every CommonJS example here is about Node, and the lesson's tests run each one in real Node.

Real-life analogyTwo ways a café serves food

At a café you order the set menu, and the plate comes out before you do anything else. In a big kitchen, the cook first reads every order on the rail. The cook checks that everything is in stock, and only then starts cooking. When the cook changes the dish on the menu board, everyone who looks at the board sees the new dish.

In real life: You order the set menu and the plate comes out now
In JavaScript: require() runs the file and gives back module.exports on that line
In real life: The cook reads every order before cooking anything
In JavaScript: ES modules find every import and export before any code runs
In real life: The menu board on the wall shows today's dish
In JavaScript: An ES module import always shows the current value
The main idea

CommonJS and ES module interop means using both kinds of module together in one Node project. "Interop" is short for "working together". One kind can load the other. This lesson shows which ways work, which need care, and which fail.

How Node chooses a loader

INTERACTIVE

A loader is the part of Node that reads a file and runs it. Node has a CommonJS loader and an ES module loader. Before it runs a file, Node picks one of them.

Node does not look for import or require inside the file to decide. It checks these things, in this order:

  1. A flag such as --input-type=module. This is only for code you pass to Node as text, which has no file name.
  2. The file ending. .cjs always means CommonJS. .mjs always means ES module.
  3. For a .js file, the type field in the nearest package.json file. "module" means ES module. "commonjs", or no type at all, means CommonJS.
Three files. tool.cjs leads to CommonJS. tool.mjs leads to ES module. tool.js leads to package.json, whose type field decides.tool.cjsCommonJStool.mjsES moduletool.jspackage.jsonits type decides
The ending .cjs always means CommonJS, and .mjs always means ES module. A .js file follows the type field in the nearest package.json.

Try it below. Each button is one file and the type in its nearest package.json. The code on the left is a short copy of Node's rules, and the line that decides is highlighted.

Which loader runs this file?
Node's rules, as a short functionPop out in the code editor (opens in a new tab)JavaScript
function decide({ file, nearestType, inputTypeFlag }) {  if (inputTypeFlag) return inputTypeFlag;  if (file.endsWith(".cjs")) return "CommonJS";  if (file.endsWith(".mjs")) return "ES module";  if (nearestType === "module") return "ES module";  return "CommonJS";}
LoaderES module
fileapp.js
nearest typemodule
loaderES module

A .js file follows the type in the nearest package.json. Here it says module.

Try it yourself

app.js runs as ES module. A .js file follows the type in the nearest package.json. Here it says module.

The code is a short copy of Node's rules for picking a loader. Browser pages can't run CommonJS files on their own.

Each row of this table is one file. Read across to see which loader Node uses for it.

Which loader Node uses for each file
FileThe type in the nearest package.jsonWhich loader Node uses
tool.cjsAny type, or none.CommonJS. The .cjs ending decides.
tool.mjsAny type, or none.ES module. The .mjs ending decides.
tool.js"module"ES module.
tool.js"commonjs", or no type at all.CommonJS.

So one project can mix both kinds. A project with type: "module" can still keep one CommonJS file named tool.cjs. A CommonJS project can keep one ES module named tool.mjs. This is why the full file name matters when you report a bug.

Two more rules

A package.json inside a sub-folder sets the type for the files in that folder. Code you pass as text, with node -e or through standard input, has no file ending. For that code, use the flag --input-type=module or --input-type=commonjs.

require and module.exports

STEP THROUGH

Node runs each CommonJS file inside a hidden function, called the wrapper. The wrapper gives the file a few local variables:

  • require is a function that loads another file.
  • module is an object about this file. Its module.exports property holds what the file shares.
  • exports is a shortcut. At the start, it points to the same object as module.exports.
  • __dirname and __filename hold the folder path and the file path of this file.

When another file calls require() on this file, it gets back module.exports.

require hands over the value it finds at that momentJavaScript
// settings.cjs
module.exports = 3;
setTimeout(() => {
  module.exports = 4;
}, 0);

// app.cjs
const value = require("./settings.cjs");
console.log(value);

// cart.cjs
module.exports = { total: 3 };

// app.cjs
const cart = require("./cart.cjs");
cart.total = 4;
console.log(cart.total);

This code has two small Node examples:

  • Lines 1 to 9: settings.cjs shares the number 3. A timer later sets module.exports to 4. But app.cjs prints 3. require is synchronous: it finishes on its own line, before the timer runs. So value already holds 3, and the later change can't reach it.
  • Lines 11 to 17: cart.cjs shares an object. app.cjs sets its total to 4 and prints 4. Both files hold the same object, so a change made through one name shows through the other.

Step through three cases below. Pick one with the buttons:

  • First require: a file is loaded for the first time.
  • Cached require: the same file is asked for twice.
  • Circular require: two files ask for each other.
Step through CommonJS loading
Step 0 of 5Ready
Your turn: follow the blue line

Step through the first time CommonJS loads a file.

Running in
  1. script
Next: line 2
// main.cjsconsole.log(cart.total([2, 3])); // cart.cjsconst math = require("./math.cjs");exports.total = (items) => items.reduce(math.add, 0); // math.cjsexports.add = (a, b) => a + b;
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick a case to step through.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Node keeps every file it has loaded in a cache, a store of files it already ran. It finds them by their full path. This has two effects:

  • Slow setup code in a file runs only once.
  • Every file that requires it gets the same module.exports object. A change made by one file shows for all of them.

In a circular require, two files require each other. Node puts a file in the cache as soon as it starts running it. So the second file can get the first file's module.exports before the first file has finished filling it in.

One more trap: exports is only a shortcut to module.exports. Adding a property through the shortcut works. Pointing exports at a new object does not change what the file shares.

exports is only a shortcut
Adding to exports works; replacing it doesn'tPop out in the code editor (opens in a new tab)JavaScript
// broken.cjsexports.answer = 42;exports = { answer: 0 };console.log(module.exports.answer); // replacement.cjsmodule.exports = function greet(name) {  return "Hello, " + name;};
Outputshortcut
module.exports.answer42
local exports.answer0
Try it yourself
Read lines 2 and 3, then compare the two values on the right.

Line 2 adds answer through the shortcut, so it reaches module.exports. Line 3 points the shortcut at a new object, which module.exports never sees. Line 7 is the right way to share a whole new value.

The values on the right come from a short copy of the variables the CommonJS wrapper gives each file.
Left: module.exports points to the original object, which holds answer: 42. require gets this object. Right: after line 3, exports points to a new object holding answer: 0. No one gets that object.module.exportsanswer: 42originalrequire() gets thisexportsanswer: 0newno one gets this
After line 3, exports points to a new object. module.exports still holds the original object with answer: 42, and that is what require() gets.

How the two kinds load each other

INTERACTIVE

An ES module can load a CommonJS file with import. The safest way is a default import, which is an import without curly braces: import pkg from "./lib.cjs". Then pkg is the CommonJS file's whole module.exports value.

The line import pkg from ./lib.cjs. A tag named pkg points to the exports cart of lib.cjs, which holds answer: 42.import pkg from "./lib.cjs"pkganswer: 42exportsmodule.exports of lib.cjs
lib.cjs sets module.exports = { answer: 42 }. A default import hands you that whole object, so pkg.answer is 42.

A named import uses curly braces, like import { answer } from "./lib.cjs". From a CommonJS file, it only works sometimes. Node has to find the name answer by reading the CommonJS file's text before the file runs. It can find simple lines like exports.answer = 42. It can't always find names that are added in other ways.

ES modules also share variables in a different way. An ES module import is a live binding: a read-only link to the exporting file's variable, not a copy of its value. When the exporting file changes the variable, the importing file sees the new value.

An ES module import shows the current valueJavaScript
// score.mjs
export let score = 1;
export function addPoint() {
  score += 1;
}

// app.mjs
import { addPoint, score } from "./score.mjs";
console.log(score);
addPoint();
console.log(score);

Run in Node, this prints 1 and then 2. Line 9 prints score, which is 1. Line 10 calls addPoint(), which adds 1 to score inside score.mjs. Line 11 reads through the same live link and prints 2. Compare this with the require example above, where value kept the number it got.

Real-life analogyA travel plug adapter

You take a travel adapter to another country. Its big main socket fits your plug every time. Its small pin holes fit only the pins they were made for. A plug with an unusual pin may not go in.

In real life: The adapter lets a plug from one country connect
In JavaScript: ES modules can import CommonJS, and CommonJS can require some ES modules
In real life: A small pin fits only a hole made for it
In JavaScript: A named import from CommonJS works only if Node found that name
In real life: The big main socket always fits
In JavaScript: A default import is always the whole module.exports

Try the four cases below. Each button shows the code and what current Node prints or throws. Version 22 of Node was used to check them.

Four ways to cross between the two kinds
Default import of CommonJSJavaScript
// lib.cjsexports.answer = 42;module.exports.extra = "visible"; // app.mjsimport pkg, { answer, extra } from "./lib.cjs";console.log(pkg.answer, answer, extra); // require-esm.cjsconst ns = require("./tool.mjs");console.log(ns.default, ns.value);
Current Node resultworks
prints or throws42

A default import gets the CommonJS file's whole module.exports value.

Try it yourself

works: A default import gets the CommonJS file's whole module.exports value.

These results come from Node 22. The lesson's tests run each case in real Node to check them.

Each row of this table is one way to cross between the two kinds. Read across to see what you get and what to watch for.

What you get when one kind loads the other, in current Node
What you writeWhat you getWhat to watch for
An ES module default-imports a CommonJS file.The exact module.exports value.Nothing. This always works.
An ES module uses a named import from a CommonJS file.Only the names Node could find in the file's text.Some ways of writing module.exports hide the names.
A CommonJS file requires an ES module.The module's namespace object: one object with all its exports.Only in current Node. It fails if the module uses top-level await.
An ES module needs require.A require made with createRequire(import.meta.url).ES modules have no require of their own.

Using require on an ES module

NODE 22

Older advice says "CommonJS can't require ES modules." Current Node is more exact. A CommonJS file can require() an ES module, as long as that module doesn't use top-level await. Top-level await is an await written outside any function, so the whole module pauses there.

require must finish on its own line. It can't pause and wait. So if the ES module, or a file it imports, uses top-level await, Node throws the error ERR_REQUIRE_ASYNC_MODULE.

This feature is new. Node added it in version 20.17.0 behind a flag, a command-line option you had to turn on. It works without a flag from Node 20.19.0 and 22.12.0. A project can still turn it off with --no-experimental-require-module. Check which Node version your server runs before you rely on it.

require for a module without await, import() for a module with awaitJavaScript
// tool.mjs
export const value = 7;
export default "esm";

// app.cjs
const namespace = require("./tool.mjs");
console.log(namespace.default, namespace.value);

// async-tool.mjs
await Promise.resolve();
export const ready = 9;

// async-app.cjs
async function loadTool() {
  const namespace = await import("./async-tool.mjs");
  console.log(namespace.ready);
}
loadTool();

This code has two Node examples:

  • Lines 1 to 7: tool.mjs has no top-level await. So require gives back its namespace object: one object that holds all the module's exports. Line 7 prints esm 7. The default export is on namespace.default, and the named export value is on namespace.value.
  • Lines 9 to 18: async-tool.mjs uses top-level await on line 10, so require would fail. Instead, line 15 uses import(), written like a function call. It gives back a promise, and await waits for it. Line 16 prints 9.

Both examples run in Node, not in this page's code editor.

The rule to remember

In current Node, require() works on an ES module without top-level await. It fails on a module with top-level await. For that case, use import() inside an async function. The Dynamic import & top-level await lesson covers import().

The namespace object from require("./tool.mjs") has the same shape as the object from import * as ns:

  • The default export is on ns.default.
  • Each named export is on its own name, like ns.value.
  • There is no exports shortcut that you could change, as there is in CommonJS.

If you can't be sure that a package has no top-level await, use import(). It gives back a promise, so the CommonJS file can wait for the module.

Dual packages: one package, two files

INTERACTIVE

A package can give different files to different users. It lists them in the exports field of its package.json, called the exports map. Each key in the map is a condition:

  • "import" names the file for code that uses import.
  • "require" names the file for code that uses require.
  • "default" names the file for anything else.

A package with one file for import and another for require is called a dual package. This helps while a package moves from CommonJS to ES modules. But there is a risk. If one app loads the package both ways, Node runs two separate files. Each file has its own variables. So any saved data, like a counter, splits into two copies. This is called the dual package hazard.

Real-life analogyTwo contact lists on one phone

Your phone has two contact lists, and your friend Asha is in both. Asha gets a new number, and you change it in one list. The other list still shows her old number. Now you can't be sure which number is right.

In real life: Two lists that both have your friend Asha
In JavaScript: An import file and a require file from one package
In real life: You change Asha's number in one list
In JavaScript: The ES module counter goes up
In real life: The other list still has the old number
In JavaScript: The CommonJS counter doesn't change
Left: import loads counter.mjs, whose count is 2. Right: require loads counter.cjs, whose count is 1. The two counts are separate.importcount: 2counter.mjsES module copyrequirecount: 1counter.cjsCommonJS copy
import gets counter.mjs and require gets counter.cjs. Each file keeps its own count, so one shows 2 and the other shows 1.

In the lab below, pick who loads the package. Node reads the conditions from top to bottom and uses the first one that fits. The panel on the right also shows the two counters: the CommonJS file added 1 once, and the ES module file added 1 twice.

Which file does each user get?
The exports map in package.jsonJSON
{  "name": "address-book",  "exports": {    "import": "./counter.mjs",    "require": "./counter.cjs",    "default": "./counter.mjs"  }}
File and counters./counter.mjs
condition usedimport
CommonJS counter1
ES module counter2
same object?false
Try it yourself

This user gets ./counter.mjs, from the "import" condition. If the import user and the require user get two different files, each file keeps its own counter.

To fix the split, make one file a small wrapper that loads the other file, or publish only an ES module.

There are two safer designs:

  • Make one file a thin wrapper: a small file that only loads the other file and shares what it got. Then both ways use one copy of the code and its data.
  • Publish only an ES module, if all your users can load ES modules.

The goal is one copy of the code for each piece of saved data.

Node helpers in each kind of module

CommonJS files get three local helpers that ES modules don't: require, __dirname, and __filename. In an ES module in Node, use these instead:

  • import.meta.dirname for the folder path. import.meta is an object with facts about the current module.
  • import.meta.filename for the file path.
  • createRequire(import.meta.url), from node:module, when you really need a CommonJS require.
CommonJS helpers and what ES modules use insteadJavaScript
// CommonJSconsole.log(typeof require, typeof __dirname, typeof __filename); // ES moduleimport { createRequire } from "node:module";const require = createRequire(import.meta.url);console.log(import.meta.dirname, import.meta.filename);console.log(typeof require);

Line 2 runs in a CommonJS file and prints function string string. Lines 5 and 6 make a require inside an ES module. Line 7 prints the folder path and the file path. Line 8 prints function.

Node added import.meta.dirname and import.meta.filename in version 20.11.0. On older Node, build the paths from import.meta.url with the fileURLToPath and dirname functions.

These are Node tools, not browser tools. A browser ES module has import.meta.url, but no file paths. A browser can't run require() either, unless a bundler (a build tool that joins your files) rewrites the code first.

Where you’ll use this

You will need these rules when:

  • A test tool says "Cannot use import statement outside a module". Node chose the CommonJS loader for a file that uses import.
  • A package's instructions show const tool = require("tool"), but your app uses ES modules.
  • Node says "Named export not found" for a CommonJS package.
  • A dual package gives you two copies of something that should exist only once.
Load a CommonJS package from an ES module, the safe wayJavaScript
import legacy from "legacy-commonjs-package";const { parse, format } = legacy; export function render(input) {  return format(parse(input));}

Line 1 default-imports the CommonJS package and calls it legacy. Line 2 takes the parse and format functions out of it. This simple pattern doesn't depend on Node finding named exports, and it is easy to read.

A checklist for mixing the two kinds
  • Find out which loader runs each file.
  • Read the package's exports map.
  • Use a default import for CommonJS packages.
  • Use import() for ES modules with top-level await.
  • If a package is dual, test both the import way and the require way.

Common misconceptions

SORTER
  • “Browsers run CommonJS.” Browsers run plain scripts and ES modules. CommonJS needs Node, or a build tool that rewrites it first.
  • “Every CommonJS property is a named ES export.” A named import from CommonJS works only for names Node can find by reading the file's text.
  • “exports = {} replaces what the file shares.” It only points the local shortcut at a new object. Use module.exports = ... instead.
  • “type: module changes every file ending.” It only changes .js files. .cjs and .mjs always keep their own loader.
  • “Dual packages are always safe.” They are safe only when both files share one copy of the code and its data.
  • “require keeps a shared number up to date.” It hands over the value it finds at load time. Only a shared object stays shared, so changes to its properties show everywhere.
  • “An ES module import copies the value.” A named import is a live link, so later changes made by the exporting file show in the importing file.
Sort the snippets
  • import pkg from "./lib.cjs";
  • import { answer } from "./lib.cjs"; // lib uses exports.answer = 42
  • import { answer } from "./lib.cjs"; // lib does module.exports = { answer: 42 }
  • const ns = require("./tool.mjs"); // no top-level await
  • require("./async-tool.mjs"); // async-tool has top-level await
  • const require = createRequire(import.meta.url);
Try it yourself
0 of 6 correct

Sort each snippet by what current Node does with it.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict a write through exports

This code copies what the CommonJS wrapper sets up. Read it without running it. What does it print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const module = { exports: {} };
const exports = module.exports;
exports.answer = 42;
console.log(module.exports.answer);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFollow the cache

    This code copies how the CommonJS cache works. Predict both values it prints.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const cache = {};
    function load() {
      if (cache.tool) return cache.tool;
      cache.tool = { count: 0 };
      return cache.tool;
    }
    const a = load();
    const b = load();
    a.count += 1;
    console.log(a === b, b.count);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix a named import that fails

      In Node, line 5 throws "Named export 'answer' not found". Type a safer line to use in its place.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      // lib.cjs
      module.exports = { answer: 42 };
      
      // app.mjs
      import { answer } from "./lib.cjs";
      console.log(answer);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · ChallengeSpot the split counter

        This code copies the dual package hazard with two plain objects. Predict what it prints.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const cjsCounter = { count: 0 };
        const esmCounter = { count: 0 };
        cjsCounter.count += 1;
        esmCounter.count += 1;
        esmCounter.count += 1;
        console.log(cjsCounter.count + "/" + esmCounter.count);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeName the async require error

          Which Node error code appears when app.cjs runs?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          // async-tool.mjs
          await Promise.resolve();
          export const value = 1;
          
          // app.cjs
          require("./async-tool.mjs");

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8A package has type: module in its package.json. Which loader runs a .cjs file in it?

              Choose an answer to see the explanation.

            2. Question 2 of 8This code copies what the CommonJS wrapper sets up. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const module = { exports: {} };
              const exports = module.exports;
              exports.answer = 42;
              console.log(module.exports.answer);

              Choose an answer to see the explanation.

            3. Question 3 of 8An ES module default-imports a CommonJS file. What is the default value?

              Choose an answer to see the explanation.

            4. Question 4 of 8In current Node, which require() of an ES module fails?

              Choose an answer to see the explanation.

            5. Question 5 of 8This code copies how the CommonJS cache works. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const cache = {};
              function load() {
                if (cache.tool) return cache.tool;
                cache.tool = { count: 0 };
                return cache.tool;
              }
              const a = load();
              const b = load();
              a.count += 1;
              console.log(a === b, b.count);

              Choose an answer to see the explanation.

            6. Question 6 of 8Why can a dual package end up with two different counts of the same thing?

              Choose an answer to see the explanation.

            7. Question 7 of 8This code copies a dual package with two plain objects. What does it print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const cjsCounter = { count: 0 };
              const esmCounter = { count: 0 };
              cjsCounter.count += 1;
              esmCounter.count += 1;
              esmCounter.count += 1;
              console.log(cjsCounter.count + "/" + esmCounter.count);

              Choose an answer to see the explanation.

            8. Question 8 of 8In an ES module, what gives you a CommonJS require?

              Choose an answer to see the explanation.

            Key takeaways

            • .cjs means CommonJS. .mjs means ES module. .js follows the type in the nearest package.json.
            • require() finishes on its own line and gives back module.exports. Node runs each CommonJS file once and keeps it in a cache.
            • A default import of CommonJS is the whole module.exports. A named import from CommonJS works only if Node finds the name.
            • In current Node, require() on an ES module gives back its namespace object. It throws if the module uses top-level await.
            • A dual package needs one shared copy of its code, or its saved data splits in two.

            Final definition.
            CommonJS and ES module interop is the set of rules Node uses when the two kinds of module load each other: CommonJS (require, module.exports), which runs files on the spot, and ES modules (import, export), which are connected before any code runs.

            Up next: Proxy.

            CompleteFrontend Clear concepts. Working examples.