Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 12 – Modules, packages and build tools

CommonJS, require(esm) and dual packages in Node

Read and write CommonJS, import it from ES modules, load ES modules with require() in Node.js 24, and publish one package for both without two copies.

  • Intermediate
  • 22 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 22 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Read and write CommonJS modules with require, module.exports and exports
  • Import CommonJS from an ES module, and predict which named exports Node.js finds
  • Load ES modules with require() in Node.js 24, and explain when it throws
  • Replace __dirname, __filename and require in ES modules with import.meta and createRequire
  • Publish one package for both import and require without loading it twice

Before you start

What you’ll learn · Before you start

What you will learn

  • Read and write CommonJS modules with require, module.exports and exports
  • Import CommonJS from an ES module, and predict which named exports Node.js finds
  • Load ES modules with require() in Node.js 24, and explain when it throws
  • Replace __dirname, __filename and require in ES modules with import.meta and createRequire
  • Publish one package for both import and require without loading it twice

Before you start

On this page

CommonJS is Node.js’s original module system. A file shares values by putting them on module.exports, and another file loads it with require(), which reads, runs and returns it synchronously, on the spot. ES modules came later and are the language’s standard, and Node.js 24 runs both systems side by side: an ES module can import a CommonJS file, and require() can load an ES module, as a stable feature since Node.js 24.15, provided no module of its graph uses top-level await. Plenty of existing code and many npm packages are CommonJS, so you will read it and mix it with ES modules. This lesson shows how CommonJS works inside, what each direction of interop gives you, what replaces __dirname in an ES module, and how a package with separate CommonJS and ES module builds gets loaded twice in one program, with the fix.

How CommonJS works

Before Node.js runs a CommonJS file, it wraps the file’s code in a function with five parameters and calls it. The parameters are the module system:

What the wrapper function gives a CommonJS file JavaScript · basics/wrapper.cjs
// What Node.js gives every CommonJS file: the five parameters of the function it wraps the file in.
/* global require, module, exports, __filename, __dirname, arguments -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const { basename } = require('node:path');

console.log('parameters of the wrapper function:', arguments.length);
console.log('exports, require, module:', typeof exports, typeof require, typeof module);
console.log('__filename:', basename(__filename), '| __dirname:', basename(__dirname));
console.log('this is module.exports:', this === module.exports);

Output

parameters of the wrapper function: 5
exports, require, module: object function object
__filename: wrapper.cjs | __dirname: basics
this is module.exports: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node wrapper.cjs

  • exports, require and module are parameters of that function, not globals, and so are __filename and __dirname, the file’s own path and folder.
  • Variables declared at the top of the file are local to the function, so they stay private to the module.
  • this at the top level is module.exports, which exports also points to at the start.

require() finds a file, runs it once and keeps its module.exports in a cache keyed by the resolved file name:

require(), its cache, and a wrong way to export

basics/main.cjs

// Requires prices.cjs twice, then a module that assigns to exports instead of module.exports.
/* global require -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const first = require('./prices.cjs');
const second = require('./prices.cjs');

console.log('₹100 with GST:', first.withGst(10000) / 100);
console.log('the same object both times:', first === second);
console.log('kept in require.cache:', Object.keys(require.cache).some((file) => file.endsWith('prices.cjs')));
console.log('wrong-exports.cjs gives:', JSON.stringify(require('./wrong-exports.cjs')));

Output

prices.cjs runs
₹100 with GST: 118
the same object both times: true
kept in require.cache: true
wrong-exports.cjs gives: {}

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.cjs

basics/prices.cjs

// A CommonJS module: whatever ends up in module.exports is what require() returns.
/* global require, module -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
console.log('prices.cjs runs');

// No extension: require() tries .js, .json and .node in turn, and finds rates.json.
const rates = require('./rates');

function withGst(paise) {
  return paise + Math.round(paise * rates.gst);
}

module.exports = { withGst };

basics/rates.json

{ "gst": 0.18 }

basics/wrong-exports.cjs

// Assigning to `exports` only rebinds a local variable: require() returns module.exports, which is still {}.
/* eslint-disable @typescript-eslint/no-unused-vars -- the assignment below is the mistake this file shows */
/* global exports: writable -- CommonJS passes these to each file (for the linter) */
exports = { gst: 0.18 };

prices.cjs runs appears once: the second require() returned the same object from require.cache. Inside prices.cjs, require('./rates') has no extension: CommonJS tries .js, .json and .node in turn, and here it found rates.json, which it parsed as JSON. wrong-exports.cjs shows the classic mistake: exports = { … } only points the local parameter at a new object, while require() returns module.exports, still the empty object. Add properties with exports.gst = 0.18, or replace the whole value with module.exports = ….

Cycles in CommonJS

When two files require each other, the second one gets the first one’s exports as they are at that moment:

Two modules that require each other

cycle/main.cjs

// Starts the cycle by requiring inventory.cjs, then uses both modules once they have finished.
/* global require -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const inventory = require('./inventory.cjs');

console.log(inventory.reserve('pen'));
console.log(inventory.reserve('notebook'));

Output

inventory.cjs starts
orders.cjs starts
orders.cjs sees these exports of inventory.cjs: stock
inventory.cjs ends
pen: reserved
notebook: out of stock

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.cjs

cycle/inventory.cjs

// inventory.cjs and orders.cjs require each other.
/* global require, exports -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
console.log('inventory.cjs starts');
exports.stock = { pen: 3, notebook: 0 };

const orders = require('./orders.cjs');

exports.reserve = (item) => orders.place(item);
console.log('inventory.cjs ends');

cycle/orders.cjs

// Required by inventory.cjs while inventory.cjs is still running.
/* global require, exports -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
console.log('orders.cjs starts');
const inventory = require('./inventory.cjs');

console.log('orders.cjs sees these exports of inventory.cjs:', Object.keys(inventory).join(', '));

exports.place = (item) => `${item}: ${inventory.stock[item] > 0 ? 'reserved' : 'out of stock'}`;

inventory.cjs was still running when orders.cjs asked for it, so the cache handed back its unfinished exports, with stock but no reserve. Nothing ran twice and nothing looped. The later calls still worked, because orders.cjs kept a reference to the same exports object, which inventory.cjs went on filling. Had inventory.cjs ended with module.exports = { stock, reserve } instead, orders.cjs would have kept the old, empty object for good. ES modules handle cycles with live bindings instead, so a let, const or class read before its module has run is a ReferenceError there rather than a silent undefined.

CommonJS and ES modules in Node.js 24
Criterion CommonJSES modules
Loading require(), synchronous, wherever it is calledimport, connected before any code runs; import() at run time
What importers get The value module.exports has at that momentLive, read-only bindings
Extension in relative paths Optional: .js, .json and .node are triedRequired
JSON files require('./rates.json')import with { type: 'json' }
Top-level await A SyntaxErrorAllowed
Own path and folder __filename, __dirnameimport.meta.filename, import.meta.dirname
Strict mode Only with 'use strict'Always

Node.js decides per file which system runs it: .cjs is CommonJS and .mjs an ES module, while a .js file follows the "type" field of the nearest package.json; the lesson on running JavaScript lists every case. The examples here use .cjs and .mjs, so they mean the same wherever they are saved. A package.json with a syntax error, such as a trailing comma, stops even a plain node app.js with ERR_INVALID_PACKAGE_CONFIG, because Node.js reads it to decide the file’s type.

An ES module imports CommonJS; CommonJS uses require() for an ES module without top-level await, and import() for any ES module.ES module.mjs, or .js under"type": "module"CommonJS module.cjs, or .js under"type": "commonjs"ES module with notop-level awaitin its graphAny ES moduleimport: the default ismodule.exports, plus thenames Node.js can findrequire():the namespaceimport():a promise forthe namespace

How the two module systems of Node.js load each other

Text description of the diagram

The diagram is a small tree of boxes joined by arrows, read from the top.

  • An ES module imports a CommonJS module with `import`. The default export is the CommonJS file's `module.exports`, and named imports work for the names Node.js can find by reading the file.
  • The CommonJS module can load an ES module whose graph has no top-level await with `require()`, which returns the module's namespace object. With top-level await anywhere in the graph, `require()` throws `ERR_REQUIRE_ASYNC_MODULE`.
  • The CommonJS module can load any ES module with `import()`, which returns a promise for the namespace.

Importing CommonJS from an ES module

An ES module can import a CommonJS file directly. The default import is always the file’s module.exports:

Two CommonJS files imported by an ES module

esm-imports-cjs/main.mjs

// An ES module importing two CommonJS files.
import rates, { gstRate } from './rates.cjs';
import * as namespace from './rates.cjs';
import built from './built.cjs';

console.log('named import from rates.cjs:', gstRate);
console.log('default import is module.exports:', JSON.stringify(rates));
console.log('names on the namespace:', Object.keys(namespace).join(', '));
console.log('built.cjs through its default import:', built.gstRate);

Output

named import from rates.cjs: 0.18
default import is module.exports: {"gstRate":0.18,"currency":"INR"}
names on the namespace: currency, default, gstRate, module.exports
built.cjs through its default import: 0.18

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.mjs

esm-imports-cjs/rates.cjs

// Exports added one property at a time: a pattern Node.js can read without running the file.
/* global exports -- CommonJS passes these to each file (for the linter) */
exports.gstRate = 0.18;
exports.currency = 'INR';

esm-imports-cjs/built.cjs

// module.exports set to whatever a function returns: its names are known only once the file has run.
/* global module -- CommonJS passes these to each file (for the linter) */
function build() {
  return { gstRate: 0.18, currency: 'INR' };
}

module.exports = build();

Named imports are a convenience on top. An ES module’s imports are connected before any code runs, so Node.js cannot run the CommonJS file to see what it exports; it reads the source instead and recognises common patterns, such as exports.gstRate = …. That worked for rates.cjs, whose namespace offers gstRate and currency, the default, and the same value once more under the name 'module.exports'. These named exports are copies made once, not live bindings. built.cjs sets module.exports to whatever a function returns, which no reading of the source can see, so a named import from it fails before anything runs:

A named import Node.js cannot find, and __dirname in an ES module

interop-errors.mjs

// Two mistakes when ES modules meet CommonJS, run with Node.js.
import { session } from './session.mjs';

session(['node esm-imports-cjs/named-from-built.mjs', 'node paths/uses-dirname.mjs']);

Output

$ node esm-imports-cjs/named-from-built.mjs
SyntaxError: Named export 'gstRate' not found. The requested module './built.cjs' is a CommonJS module, which may not support all module.exports as named exports.
[exit status 1]
$ node paths/uses-dirname.mjs
ReferenceError: __dirname is not defined in ES module scope
[exit status 1]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node interop-errors.mjs

esm-imports-cjs/named-from-built.mjs

// A named import from built.cjs, whose exports static analysis cannot see.
import { gstRate } from './built.cjs';

console.log(gstRate);

paths/uses-dirname.mjs

// CommonJS habits in an ES module.
// eslint-disable-next-line no-undef -- the mistake this file shows
console.log(__dirname);

The full message goes on to suggest the fix: import the default and take the property from it, as main.mjs did with built.gstRate. When a named import from a CommonJS package fails, that is the answer that always works.

require() of ES modules in Node.js 24

The other direction used to be impossible: require() of an ES module threw ERR_REQUIRE_ESM. Node.js added it behind a flag in 22.0 and 20.17, turned it on by default in 23.0, 22.12 and 20.19, and declared it stable in 24.15. process.features.require_module tells a program whether it is available.

CommonJS requiring three ES modules

require-esm/main.cjs

// CommonJS code loading three ES modules with require() in Node.js 24.
/* global require -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const money = require('./money.mjs');
console.log('require(money.mjs) gives a namespace with:', Object.keys(money).join(', '));
console.log('new money.default(500).paise:', new money.default(500).paise);

const price = require('./price.mjs');
console.log("price.mjs exports 'module.exports', so require() gives:", typeof price, price(4999));

try {
  require('./slow.mjs');
} catch (error) {
  console.log('require(slow.mjs):', error.code);
}
console.log('process.features.require_module:', process.features.require_module);

Output

require(money.mjs) gives a namespace with: __esModule, currency, default
new money.default(500).paise: 500
price.mjs exports 'module.exports', so require() gives: function ₹49.99
require(slow.mjs): ERR_REQUIRE_ASYNC_MODULE
process.features.require_module: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node main.cjs

require-esm/money.mjs

// An ES module with a default export and a named export.
export default class Money {
  constructor(paise) {
    this.paise = paise;
  }
}

export const currency = 'INR';

require-esm/price.mjs

// An ES module that tells require() what to return: the export named 'module.exports'.
export default function price(paise) {
  return `₹${(paise / 100).toFixed(2)}`;
}

export { price as 'module.exports' };

require-esm/slow.mjs

// An ES module with top-level await.
await Promise.resolve();

export const ready = true;
  • require() returns the module’s namespace object, so a default export is .default. Node.js adds __esModule: true when the module has a default export, a marker that code compiled from ES modules to CommonJS by build tools looks for; code you write by hand should not depend on it.
  • An ES module can choose what require() returns by exporting a value under the string name 'module.exports', as price.mjs does. CommonJS callers then get the function itself, but they lose the module’s other named exports unless those are attached to that value.
  • slow.mjs uses top-level await, and require() refused it with ERR_REQUIRE_ASYNC_MODULE: a synchronous call cannot wait for a module graph that waits. The same error comes from top-level await anywhere in the graph, not only in the file you require. CommonJS code can still use import() for such modules.

__dirname, __filename and require() in ES modules

ES modules have no wrapper function, so the five CommonJS variables do not exist in them. The session above showed the error you get, ReferenceError: __dirname is not defined in ES module scope. Here is what replaces them:

Paths and require() in an ES module

paths/paths.mjs

// What replaces __dirname, __filename and require() in an ES module.
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { basename, join } from 'node:path';

console.log('import.meta.filename ends with:', basename(import.meta.filename));
console.log('import.meta.dirname ends with:', basename(import.meta.dirname));

const greeting = readFileSync(join(import.meta.dirname, 'greeting.txt'), 'utf8').trim();
console.log('read next to the module:', greeting);
console.log('the same through a URL:', readFileSync(new URL('./greeting.txt', import.meta.url), 'utf8').trim() === greeting);

const require = createRequire(import.meta.url);
console.log('createRequire() loads JSON synchronously:', require('./settings.json').currency);
console.log('typeof __dirname here:', typeof __dirname);

Output

import.meta.filename ends with: paths.mjs
import.meta.dirname ends with: paths
read next to the module: Namaste from a file next to the module
the same through a URL: true
createRequire() loads JSON synchronously: INR
typeof __dirname here: undefined

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node paths.mjs

paths/greeting.txt

Namaste from a file next to the module

paths/settings.json

{ "currency": "INR" }

import.meta.filename and import.meta.dirname are the module’s own path and folder (Node.js 20.11 and later), and import.meta.url is its file: URL, which new URL('./greeting.txt', import.meta.url) turns into the address of a file next to it. Both point at the module’s folder, not the folder the program was started from, so a script finds its files wherever someone runs it. When an ES module needs a synchronous require(), for example for a package that is CommonJS only, createRequire(import.meta.url) makes one that resolves relative to the module.

One package for import and require

A package’s "exports" field in package.json lists its public entry points. Anything it does not list cannot be imported by its package name, which keeps internal files internal. An entry can depend on conditions, which Node.js checks in the order they are written: "import" matches import and import(), "require" matches require(), "module-sync" matches both for an ES module without top-level await, "node" matches any Node.js, and "default" matches everything and goes last. The "imports" field adds private aliases that start with #, usable only inside the package.

The tempting design is two builds, one ES module behind "import" and one CommonJS file behind "require". Here both versions of a package called paise are used by an app that has an ES module and a CommonJS file, each using the package by its own name, which a package with "exports" may do:

Two builds of one package, and one build

hazard/package.json · dual/hazard/package.json

{
  "name": "paise",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./money.mjs",
      "require": "./money.cjs"
    }
  }
}

hazard/money.mjs · dual/hazard/money.mjs

// The ES module build of the package.
console.log('money.mjs runs');

export class Money {
  constructor(paise) {
    this.paise = paise;
  }
}

hazard/money.cjs · dual/hazard/money.cjs

// The CommonJS build of the same package: the same class, written again.
/* global module -- CommonJS passes these to each file (for the linter) */
console.log('money.cjs runs');

class Money {
  constructor(paise) {
    this.paise = paise;
  }
}

module.exports = { Money };

hazard/legacy.cjs · dual/hazard/legacy.cjs

// CommonJS code that uses the package by its name; require() takes the "require" condition of "exports".
/* global require, module -- CommonJS passes these to each file (for the linter) */
/* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
const { Money } = require('paise');

module.exports = { price: new Money(4999) };

hazard/app.mjs · dual/hazard/app.mjs

// An ES module that uses the package by its name (a package may import itself), and the CommonJS file too.
import { Money } from 'paise';
import legacy from './legacy.cjs';

console.log('legacy.price instanceof Money:', legacy.price instanceof Money);

fixed/package.json · dual/fixed/package.json

{
  "name": "paise",
  "version": "2.0.0",
  "type": "module",
  "exports": {
    ".": "./money.mjs"
  },
  "imports": {
    "#format": "./format.mjs"
  }
}

fixed/money.mjs · dual/fixed/money.mjs

// The only build of the package, an ES module. "#format" is a private alias from the "imports" field.
import { rupees } from '#format';

console.log('money.mjs runs');

export class Money {
  constructor(paise) {
    this.paise = paise;
  }

  toString() {
    return rupees(this.paise);
  }
}

fixed/peek.mjs · dual/fixed/peek.mjs

// Tries to import a file that the package's "exports" does not list.
import { rupees } from 'paise/format.mjs';

console.log(rupees(100));
Both versions run with Node.js JavaScript · dual/run.mjs
// Both versions of the package, run with Node.js, and an import of a file the package does not export.
import { session } from '../session.mjs';

session(['node hazard/app.mjs', 'node fixed/app.mjs', 'node fixed/peek.mjs']);

Output

$ node hazard/app.mjs
money.mjs runs
money.cjs runs
legacy.price instanceof Money: false
$ node fixed/app.mjs
money.mjs runs
legacy.price instanceof Money: true
legacy.price is ₹49.99
$ node fixed/peek.mjs
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './format.mjs' is not defined by "exports" in ./fixed/package.json imported from ./fixed/peek.mjs
[exit status 1]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs

With two builds, both files ran, so the program held two unrelated Money classes, and a price made by the CommonJS side was not an instanceof the class the ES module side imported. The same split doubles anything a package keeps in module state: a cache, a registry of plug-ins or a configuration set by one side is invisible to the other. This is the dual package hazard. In the fixed version, "exports" sends everyone to one ES module, require() loads that same module, and the check is true. fixed/peek.mjs also shows the encapsulation: paise/format.mjs is not in "exports", so importing it by the package name fails with ERR_PACKAGE_PATH_NOT_EXPORTED, while the package itself reaches it through its private #format alias.

So for a new package, publish ES modules only and let require() load them, which Node.js 20.19, 22.12 and every later release can do. Keep top-level await out of the package’s entry points, or CommonJS users get ERR_REQUIRE_ASYNC_MODULE. If you must keep a CommonJS build for older Node.js versions, keep the real code, and any state, in one of the two builds and make the other a thin wrapper around it, so that only one copy can exist.

Exercise: a tiny require() with a cache

makeRequire() should run each module’s function once, keep its exports in a cache, hand a cycle the exports filled in so far, throw MODULE_NOT_FOUND for unknown names, and forget a module whose function threw. The sample tests check each of those.

Exercise · Medium · JavaScript

Build a tiny CommonJS require() with a module cache

Node.js runs a CommonJS file as a function, (exports, require, module, __filename, __dirname) => { … }, and keeps every module it has loaded in a cache. Rebuild the core of that. makeRequire(modules) takes an object that maps module names to such functions, here with three parameters, (exports, require, module), and returns a require(name) that:

  • the first time a name is required, makes module = { exports: {} }, calls the module's function with module.exports, the same require and module, and returns module.exports, so exports.rate = 0.18 and module.exports = fn both work;
  • returns the cached module.exports on later calls, without calling the function again;
  • returns the exports filled in so far when a module requires another module that is still running, so two modules that require each other finish instead of looping for ever;
  • throws an Error whose code is 'MODULE_NOT_FOUND' for a name it does not know;
  • forgets a module whose function threw, so that requiring it again runs it again, as Node.js does.

Each require made by makeRequire() has its own cache. Run the sample tests when you are done.

Starter code · mini-require.mjs

// mini-require.mjs: the core of CommonJS's require() (the exercise of lesson javascript/commonjs-and-interop).
export function makeRequire(modules) {
  function require(name) {
    const module = { exports: {} };
    modules[name](module.exports, require, module);
    return module.exports;
  }
  return require;
}
The sample tests · mini-require.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { makeRequire } from './mini-require.mjs';

test('returns module.exports, filled through exports or replaced', () => {
  const require = makeRequire({
    rates: (exports) => {
      exports.gst = 0.18;
    },
    add: (exports, require, module) => {
      module.exports = (a, b) => a + b;
    },
  });
  assert.deepEqual(require('rates'), { gst: 0.18 });
  assert.equal(require('add')(2, 3), 5);
});

test('runs each module once and returns the cached exports', () => {
  let runs = 0;
  const require = makeRequire({
    counter: (exports) => {
      runs += 1;
      exports.count = 0;
    },
  });
  const first = require('counter');
  const second = require('counter');
  assert.equal(first, second);
  assert.equal(runs, 1);
});

test('a cycle gets the exports filled in so far', () => {
  // Without a cache the two modules would require each other for ever; stop after a few runs instead.
  let runs = 0;
  const guard = () => {
    runs += 1;
    if (runs > 4) throw new Error('the two modules keep running each other: is a module cached before its function runs?');
  };
  const require = makeRequire({
    inventory: (exports, require) => {
      guard();
      exports.stock = 3;
      const orders = require('orders');
      exports.sawOrders = typeof orders.place;
    },
    orders: (exports, require) => {
      guard();
      const inventory = require('inventory');
      exports.place = () => 'placed';
      exports.sawStock = inventory.stock;
      exports.sawSawOrders = inventory.sawOrders;
    },
  });
  const inventory = require('inventory');
  assert.equal(inventory.sawOrders, 'function');
  assert.equal(require('orders').sawStock, 3);
  assert.equal(require('orders').sawSawOrders, undefined);
  assert.equal(runs, 2);
});

test('an unknown name throws an Error with code MODULE_NOT_FOUND', () => {
  const require = makeRequire({});
  try {
    require('missing');
    assert.ok(false, 'require("missing") should throw');
  } catch (error) {
    assert.equal(error.code, 'MODULE_NOT_FOUND');
    assert.ok(error instanceof Error);
  }
});

test('a module that threw is run again on the next require', () => {
  let runs = 0;
  const require = makeRequire({
    flaky: (exports) => {
      runs += 1;
      if (runs === 1) throw new Error('first run fails');
      exports.ok = true;
    },
  });
  assert.throws(() => require('flaky'), Error);
  assert.deepEqual(require('flaky'), { ok: true });
  assert.equal(runs, 2);
});

test('each require has its own cache', () => {
  let runs = 0;
  const modules = { once: () => (runs += 1) };
  makeRequire(modules)('once');
  makeRequire(modules)('once');
  assert.equal(runs, 2);
});
A hint

Keep a Map from name to module object inside makeRequire(), and look a name up there before anything else. Put the new module into the map before you call its function: that is what lets a cycle find the half-finished exports instead of running the modules again and again. Wrap the call in try, and in catch delete the name from the map and throw the error again.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does node main.cjs print for the inventory and orders cycle?

    What does this program print? Choose one answer.

    // Starts the cycle by requiring inventory.cjs, then uses both modules once they have finished.
    /* global require -- CommonJS passes these to each file (for the linter) */
    /* eslint-disable @typescript-eslint/no-require-imports -- CommonJS on purpose */
    const inventory = require('./inventory.cjs');
    
    console.log(inventory.reserve('pen'));
    console.log(inventory.reserve('notebook'));
    Show the answer to question 1

    Answer: it prints

    inventory.cjs starts
    orders.cjs starts
    orders.cjs sees these exports of inventory.cjs: stock
    inventory.cjs ends
    pen: reserved
    notebook: out of stock

    When orders.cjs requires inventory.cjs, that module is still running, so the cache gives back its exports as they are at that moment: only stock. Nothing runs twice. The later calls work because orders.cjs kept a reference to the same exports object, which inventory.cjs went on filling.

  2. Question 2 of 6 A CommonJS file contains only exports = { gst: 0.18 };. What does require() return for it?

    Choose one answer.

    Show the answer to question 2

    Answer: An empty object

    exports is a parameter that starts out pointing at module.exports. Assigning to it changes only the local variable, and require() returns module.exports, which is still the empty object it was. Write module.exports = { gst: 0.18 }, or add properties with exports.gst = 0.18.

  3. Question 3 of 6 import { gstRate } from './built.cjs' fails with "Named export 'gstRate' not found", but built.gstRate works after a default import. Why?

    Choose one answer.

    Show the answer to question 3

    Answer: Node.js finds a CommonJS file's named exports by reading its code, and it cannot see names that a function call creates

    The named exports of a CommonJS module have to be known before it runs, so Node.js reads its source for patterns such as exports.gstRate = …. module.exports = build() hides the names until it runs. The default import, which is module.exports, always works.

  4. Question 4 of 6 In Node.js 24, a CommonJS file calls require('./client.mjs'), and a module that client.mjs imports uses top-level await. What happens?

    Choose one answer.

    Show the answer to question 4

    Answer: require() throws an error with the code ERR_REQUIRE_ASYNC_MODULE

    require() is synchronous, so it can only return a module graph that finishes without waiting. Top-level await anywhere in that graph makes it throw ERR_REQUIRE_ASYNC_MODULE; import() works instead.

  5. Question 5 of 6 A package's "exports" sends import to money.mjs and require to money.cjs, two builds of the same class. An app uses both. What did legacy.price instanceof Money give in the lesson, and why?

    Choose one answer.

    Show the answer to question 5

    Answer: false, because two copies of the package ran, each with its own Money class

    The conditions picked two different files, so both builds ran (each printed its line) and the program held two unrelated Money classes. Pointing both conditions at one ES module, which require() can load in Node.js 24, left one class and gave true.

  6. Question 6 of 6 What replaces this CommonJS line in an ES module?

    Read the code, then choose one answer.

    const template = fs.readFileSync(path.join(__dirname, 'mail.html'), 'utf8');
    Show the answer to question 6

    Answer: fs.readFileSync(path.join(import.meta.dirname, 'mail.html'), 'utf8')

    ES modules have no __dirname; import.meta.dirname is the folder of the module itself (Node.js 20.11 and later). process.cwd() and a relative path both depend on where the program was started from, which is a different folder as soon as someone runs the script from elsewhere.

Interview questions

Fresher · What is the difference between module.exports and exports in CommonJS?

module.exports is the value that require() returns for a file. exports is a parameter of the wrapper function that starts out pointing at the same object, as a shortcut. Adding properties through either one is the same thing: exports.rate = 0.18 puts rate on module.exports. Assigning to them is not: module.exports = fn replaces what the file exports, while exports = fn only points the local parameter at something else, and require() still returns the original, empty object. So use exports.name = … to add exports, use module.exports = … to export one function or class, and never assign to exports itself. After module.exports has been replaced, exports no longer points at it either.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    Your library ships an ES module build and a CommonJS build behind the "import" and "require" conditions. A user reports that error instanceof LibraryError is false for errors thrown by their CommonJS plug-in, and that plug-ins registered in one part of their app are missing in another. Explain, prove it, and redesign the package.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    After moving a service to ES modules, import { parse } from 'legacy-config' fails at start-up with "Named export 'parse' not found", although require('legacy-config').parse works. The package is CommonJS and ends with module.exports = createApi(). Explain why, give two fixes, and say which you would choose.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A CommonJS file runs inside a function whose parameters are exports, require, module, __filename and __dirname; require() runs a file once and caches its module.exports.
  • In a cycle, CommonJS hands back the exports filled in so far; replacing module.exports at the end leaves the other module with the old object.
  • An ES module imports CommonJS as a default export, module.exports, plus the named exports Node.js can find by reading the source; fall back to the default import when a name is not found.
  • In Node.js 24, require() loads ES modules and returns their namespace, unless top-level await appears anywhere in the graph (ERR_REQUIRE_ASYNC_MODULE).
  • In ES modules, use import.meta.dirname, import.meta.filename, import.meta.url and createRequire().
  • Separate CommonJS and ES module builds behind "require" and "import" load a package twice; publish one ES module build, or make one build a wrapper around the other.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.