CommonJS Modules
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with
the differences of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; all
versions and stability levels were checked on 2026-10-03 and every example below was run on all three lines.
CommonJS (CJS) is the module system Node.js shipped with, and the one behind most of the code in node_modules. The page
"Modules: CommonJS modules" is Stability 2 Stable, and so are require(), module, exports, filename, dirname,
require.resolve(), require.cache and require.main. The exceptions are called out where they appear: folders as modules
are Stability 3 Legacy, require.extensions and module.parent are Stability 0 Deprecated, package maps are Stability 1
Experimental, and importing native add-ons with import is Stability 1.2 Release candidate. Even if every new file in the
Bookshelf application that grows across this section is written as an ES module (see ECMAScript Modules),
you will keep meeting CommonJS: in dependencies, in older code bases, in tools, and in the error messages that mention
require. The syntax overview lives in the JavaScript modules page; this
page owns how Node.js loads, resolves and caches CommonJS.
What CommonJS is, and how Node.js implements it
In Node.js every file is a separate module with its own scope. A module publishes values by assigning to exports or
module.exports, and another module obtains them by calling require() synchronously with a specifier: a built-in module name, a
relative or absolute path, or a package name. The documentation states that the CommonJS loader is implemented in the module
core module. It is JavaScript that ships inside the Node.js binary (built-in modules live in the lib/ folder of the Node.js
source). It uses the file system to find and read the file, V8 to compile the source, and, for .node files, the operating
system’s dynamic loader. The loader is synchronous, which is why require() returns the exported value directly and why a module
that needs asynchronous initialisation has to export a function or a promise instead. The runtime layers are described in
Introduction & Architecture.
| API | What it is | Stability |
|---|---|---|
|
Load a module, find its file name without loading it, list the lookup directories. |
2 Stable |
|
The module scope variables supplied by the wrapper. |
2 Stable |
|
The module cache and the entry-point module. |
2 Stable |
|
Properties and methods of the |
2 Stable (documented on the stable CommonJS page) |
Folders as modules ( |
Passing a directory to |
3 Legacy. Use subpath exports or subpath imports instead. |
|
The table of loaders per file extension. |
0 Deprecated since v0.10.6 |
|
The first module that required this one. |
0 Deprecated (DEP0144, documentation-only; |
|
Older alias of |
Deprecated (DEP0138, documentation-only) |
|
Loads a synchronous ES module graph. See Loading ES modules with |
2 Stable; no longer experimental in v25.4.0 |
Package maps ( |
Resolve bare specifiers from a JSON file instead of the |
1 Experimental |
|
Loading a native add-on from an ES module. |
1.2 Release candidate |
The meaning of the stability indices is explained once for the whole section in Globals & Web Platform APIs.
When is a file CommonJS?
Node.js decides per file. A file is CommonJS when it has a .cjs extension, or when it has a .js extension (or none) and
the nearest parent package.json has "type": "commonjs". If there is no "type" field, or no package.json at all, a .js
file is also treated as CommonJS, unless it contains syntax that only parses as an ES module (import, export, top-level
await, import.meta); in that case syntax detection runs it as an ES module instead. The documentation recommends that
package authors always write the "type" field, even for pure CommonJS code, so that tools do not have to guess. Calling
require() always uses the CommonJS loader and calling import() always uses the ES module loader, whatever the file looks like.
$ cat detect.js
import { basename } from 'node:path'; console.log('ESM by detection, require is', typeof require, basename(import.meta.filename));
$ node detect.js # no package.json above: detected as an ES module
ESM by detection, require is undefined detect.js
$ cat typed/package.json
{ "type": "commonjs" }
$ node typed/detect.js # same file, explicit CommonJS: no detection
SyntaxError: Cannot use import statement outside a module
The same output came out of Node.js 26, 24 and 22. A file that detection turned into an ES module has no require, no
module and no exports, which is the most common reason for the message require is not defined in ES module scope after
a refactor. The complete decision procedure is on ECMAScript Modules.
The module wrapper and the module scope
Before a module’s code runs, Node.js wraps it in a function that receives five parameters:
exports, require, module, filename and dirname. That is why top-level var, let and const declarations are
private to the file instead of becoming properties of the global object, and why these five names look like globals but
differ in every module.
// wrapper.js
const path = require('node:path');
console.log('arguments.length:', arguments.length);
console.log('parameters:', typeof exports, typeof require, typeof module, typeof __filename, typeof __dirname);
console.log('this === exports:', this === exports, '| exports === module.exports:', exports === module.exports);
console.log('__filename:', path.basename(__filename), '| __dirname:', path.basename(__dirname));
var title = 'Dune';
console.log('var title is module-private:', typeof globalThis.title === 'undefined');
arguments.length: 5
parameters: object function object string string
this === exports: true | exports === module.exports: true
__filename: wrapper.js | __dirname: ex1
var title is module-private: true
Two consequences of the function wrapper are worth knowing. A top-level return is legal in a CommonJS file (it ends the module
early; it was run on Node.js 26 and 22), and top-level await is not: in a package marked "type": "commonjs" it raises
SyntaxError: await is only valid in async functions and the top level bodies of modules. Top-level this is module.exports,
not globalThis.
| Variable | Meaning | Details |
|---|---|---|
|
Absolute path of the current module file. |
Symbolic links are resolved, so for the main program it is not necessarily the name typed on the command line. For code inside |
|
Directory of the current module. |
Same as |
The symbolic-link rule was measured: running node link/prog.js, where link is a symbolic link to the directory real, printed
real/prog.js for filename. The module loader uses the real path both as the identity of the module and as the starting
point for finding its dependencies; --preserve-symlinks changes that for the dependency lookup, see
CLI Options, Environment & Config File. In ES modules
filename and __dirname do not exist; use import.meta.filename and import.meta.dirname.
exports versus module.exports
Each module starts with module.exports set to an empty object, and exports is only a variable that was initialised with a
reference to that same object. require() returns whatever module.exports holds when the module finishes. So adding
properties through either name works, while assigning to exports merely rebinds a local variable.
// shelf.js
exports.name = 'shelf'; // adds a property to the original object
exports = { name: 'lost' }; // rebinds the local variable only
module.exports.kind = 'wooden'; // still the original object
// counter.js: replacing module.exports with a function (a factory)
let created = 0;
module.exports = function createCounter() {
created += 1;
let n = 0;
return { next: () => ++n, id: created };
};
module.exports.version = 2;
// main.js
const shelf = require('./shelf.js');
console.log('shelf:', shelf);
const createCounter = require('./counter.js');
const c1 = createCounter();
const c2 = createCounter();
console.log(c1.next(), c1.next(), c2.next(), 'ids', c1.id, c2.id, 'version', createCounter.version);
shelf: { name: 'shelf', kind: 'wooden' }
1 2 1 ids 1 2 version 2
The module-level created variable survives between calls because the module executes only once (see Caching); each
createCounter() call gets its own n. When you replace module.exports with a function or class, attach extra properties to
that new value (as version above), or write the idiom module.exports = exports = … if later lines still use exports.
| Goal | Form |
|---|---|
Several named values |
|
One function, class or instance |
|
Add a property to a function export |
Assign it after the |
A value that is only known later |
Export a function that returns a promise; do not assign inside a callback |
Assignment to module.exports must be synchronous. If the assignment happens inside a callback, whoever required the module
already holds the old object:
// late.js
setTimeout(() => { module.exports = { a: 'hello' }; }, 0);
// consumer
const late = require('./late.js');
console.log('late.a right away:', late.a);
setTimeout(() => console.log('late.a afterwards:', late.a, '| fresh require:', require('./late.js').a), 10);
late.a right away: undefined
late.a afterwards: undefined | fresh require: hello
The cache keeps the module object, not a copy of the export, so a later require() sees the new value while the consumer that
already destructured or stored the old one never does. Neither exports nor module exist in an ES module; code that mixes
them with import fails with a ReferenceError.
Resolution: how require() finds a file
require(X) called from a module in directory Y follows a fixed procedure. The documentation gives it in pseudocode
(require(X) from module at path Y); the table is the same procedure in prose, in the order it is applied.
| Step | Condition | What happens |
|---|---|---|
Built-in |
|
The built-in module is returned and nothing else is searched. With the |
Path |
|
Try |
Package imports |
|
Look up the |
Self-reference |
|
Resolve through that package’s own |
Package map |
A package map is active (Experimental, Node.js 26.4.0 and later) |
Resolve the package from the map instead of from |
|
Anything else (a bare specifier) |
Walk the |
| Form | Rule |
|---|---|
File |
Use the exact name if the file exists, and decide its format from its extension (and |
Directory |
Read |
Package with |
When a package found in |
The node_modules walk
For a bare specifier such as isbn, Node.js starts at the directory of the calling module, appends /node_modules, and tries to
find the package there. If it is not found, it moves to the parent directory and repeats, until the root of the file system.
Node.js never appends node_modules to a path that already ends in node_modules. A sub path after the package name resolves
relative to the package (example-module/path/to/file), and scoped packages (@shelf/tools) are split into scope, name and
sub path first. The loader looks up the real path of every module it loads and then searches the node_modules folders of that
real path, which is what lets package managers install packages as symbolic links.
The scratch project for this page has this layout under one directory, ex3:
ex3/
resolve.js
lib/catalog.js lib/catalog.json lib/deep/where.js
node_modules/isbn/ package.json (main: ./lib/isbn.js) lib/isbn.js lib/extra.js
node_modules/@shelf/tools/ package.json, index.js
require.resolve() and its paths option// resolve.js
const path = require('node:path');
const rel = (p) => path.relative(__dirname, p);
console.log('./lib/catalog ->', rel(require.resolve('./lib/catalog'))); // .js is tried first
console.log('./lib/catalog.json->', rel(require.resolve('./lib/catalog.json')));
console.log('isbn ->', rel(require.resolve('isbn'))); // package.json "main"
console.log('isbn/lib/extra ->', rel(require.resolve('isbn/lib/extra'))); // sub path, no "exports" field
console.log('@shelf/tools ->', rel(require.resolve('@shelf/tools'))); // index.js fallback
console.log('node:fs ->', require.resolve('node:fs'));
console.log('with paths option ->', rel(require.resolve('isbn', { paths: [path.join(__dirname, 'lib', 'deep')] })));
try { require.resolve('./lib/missing'); } catch (err) {
console.log(err.code, '|', err.message.split('\n')[0].replace(__dirname, '<dir>'));
console.log('requireStack:', err.requireStack.map(rel));
}
./lib/catalog -> lib/catalog.js
./lib/catalog.json-> lib/catalog.json
isbn -> node_modules/isbn/lib/isbn.js
isbn/lib/extra -> node_modules/isbn/lib/extra.js
@shelf/tools -> node_modules/@shelf/tools/index.js
node:fs -> node:fs
with paths option -> node_modules/isbn/lib/isbn.js
MODULE_NOT_FOUND | Cannot find module './lib/missing'
requireStack: [ 'resolve.js' ]
require.resolve() runs the real lookup but returns the file name instead of executing the file, so it is the right tool to
answer "which copy of this package am I getting?", to check that an optional dependency exists, and to find a package’s
directory. Its paths option replaces the default starting points (each entry starts a node_modules walk), except that the
global folders are always included. When nothing matches, it throws an error whose code is MODULE_NOT_FOUND and whose
requireStack property lists the modules that led to the failing call.
require.resolve.paths(request) returns the directories that would be searched, or null for a built-in module:
// lib/deep/where.js
const path = require('node:path');
const rel = (p) => path.relative(path.join(__dirname, '..', '..'), p);
console.log('module.paths:');
for (const p of module.paths.slice(0, 3)) console.log(' ', rel(p) || '.');
console.log('require.resolve.paths("isbn") first 3:');
for (const p of require.resolve.paths('isbn').slice(0, 3)) console.log(' ', rel(p));
console.log('node:fs ->', require.resolve.paths('node:fs'), '| fs ->', require.resolve.paths('fs'));
module.paths:
lib/deep/node_modules
lib/node_modules
node_modules
require.resolve.paths("isbn") first 3:
lib/deep/node_modules
lib/node_modules
node_modules
node:fs -> null | fs -> null
The paths are relative to the scratch directory here; real output is absolute and continues up to /node_modules, followed by the global
folders. module.paths holds the same search list for the current module. The next listing shows what a package’s
"exports" field does to a lookup:
{ "name": "sealed", "main": "./lib/old.js", "exports": { ".": "./lib/new.js", "./extra": "./lib/extra.js" } }
console.log(require('sealed'), '|', require('sealed/extra'));
try { require('sealed/lib/private'); } catch (err) { console.log(err.code); }
new entry | extra
ERR_PACKAGE_PATH_NOT_EXPORTED
Global folders and NODE_PATH
If a bare specifier is found nowhere in the node_modules walk, Node.js searches the directories listed in the NODE_PATH
environment variable and then the historic global folders $HOME/.node_modules, $HOME/.node_libraries and
$PREFIX/lib/node. The documentation says to prefer the local node_modules folder, which is faster and more reliable.
NODE_PATH is a list of absolute paths separated by a colon on Unix and a semicolon on Windows, and it only affects
CommonJS resolution. The order was measured with one package present both in NODE_PATH and in node_modules: the local copy won, and
a path list separated by a comma found nothing:
$ NODE_PATH=$PWD/shared:$PWD/shared2 node app/np.js # app/node_modules/tag exists; shared/tag exists too
from node_modules
second path
$ NODE_PATH=$PWD/shared,$PWD/shared2 node app/np.js
from node_modules
MODULE_NOT_FOUND
This was run on Node.js 26. The ES module resolver ignores NODE_PATH completely; the "imports" field is the modern way to get short
internal specifiers, see package.json, exports & imports.
Built-in modules and the node: prefix
Built-in modules are compiled into the binary. require('http') always returns the built-in module even if a file or package
named http exists, and the node: prefix (supported by require() since v16.0.0 and v14.18.0) says the same thing
explicitly and also bypasses require.cache. A few modules can only be loaded with the prefix, so that new built-ins cannot
collide with packages that already use the name. module.builtinModules lists them, and since v23.5.0 that list includes the
prefix-only ones.
const { builtinModules } = require('node:module');
const prefixed = builtinModules.filter((m) => m.startsWith('node:'));
console.log('node:-only built-ins:', prefixed);
for (const name of ['sqlite', 'test', 'node:sqlite']) {
try { require(name); console.log(name, 'loads'); } catch (err) { console.log(name, '->', err.code); }
}
console.log("'fs' === 'node:fs':", require('fs') === require('node:fs'));
| Node.js | prefixed |
|---|---|
26.10.0 |
|
24.21.0 |
|
22.23.3 |
empty list: |
On all three lines sqlite and test fail with MODULE_NOT_FOUND, node:sqlite loads and require('fs') === require('node:fs')
is true. Always writing the prefix (node:fs) makes code immune to a user-land package shadowing the name, and it is
the form used throughout this section.
Caching
A module is executed the first time it is required; the module object is stored in require.cache, keyed by the resolved
file name, and every later require() that resolves to the same file returns the same module.exports. This makes modules
behave like singletons, lets cycles terminate (see Circular dependencies), and means that to run code several times a module must export
a function.
module.children, deleting a cache entry, and the node: bypass// stamp.js
console.log('stamp.js is executing');
module.exports = { loadedAt: Math.random().toFixed(6) };
// cache.js
const a = require('./stamp');
const b = require('./stamp.js');
console.log('same object:', a === b);
const key = require.resolve('./stamp');
console.log('cache key is the resolved filename:', require('node:path').basename(key), key in require.cache);
console.log('module.children:', module.children.map((m) => require('node:path').basename(m.filename)));
delete require.cache[key];
const c = require('./stamp');
console.log('after delete, same object:', a === c);
// node: prefix and the cache
const fakeFs = {};
require.cache.fs = { exports: fakeFs };
console.log("require('fs') is fake:", require('fs') === fakeFs, "| require('node:fs') is real:", require('node:fs') !== fakeFs);
stamp.js is executing
same object: true
cache key is the resolved filename: stamp.js true
module.children: [ 'stamp.js' ]
stamp.js is executing
after delete, same object: false
require('fs') is fake: true | require('node:fs') is real: true
Deleting a key from require.cache makes the next require() load a fresh copy; this was the basis of older hot-reload
helpers, and node --watch (see Running Scripts, the CLI & the REPL)
restarts the process instead, which is safer because code that already holds references to the old module keeps them. Deleting
does not work for native add-ons (reloading them is an error), and adding entries that shadow a built-in name is possible, which
is why the documentation says "use with care". Test doubles are better built with the test runner’s mocks.
| Situation | Effect |
|---|---|
The same package name resolves to different files |
The cache key is the resolved file name, so two copies of one package installed under different |
Case-insensitive file systems |
|
The second row was reproduced on macOS (APFS, case-insensitive by default), on Node.js 26, 24 and 22:
// foo.js prints 'Foo.js executing' when run
const a = require('./foo');
const b = require('./FOO');
console.log('same object:', a === b, '| cache entries for foo:', Object.keys(require.cache).filter((k) => /foo\.js$/i.test(k)).length);
Foo.js executing
Foo.js executing
same object: false | cache entries for foo: 2
Circular dependencies
When module A requires B and B requires A, Node.js hands B the unfinished exports object of A, exactly as it is at that
moment, instead of looping forever. Whatever A had assigned before calling require('./book.js') is visible; the rest is not.
// author.js
console.log('author: start');
exports.ready = false;
const book = require('./book.js');
console.log('author: book.ready is', book.ready);
exports.ready = true;
console.log('author: end');
// book.js
console.log('book: start');
exports.ready = false;
const author = require('./author.js');
console.log('book: author.ready is', author.ready);
exports.ready = true;
console.log('book: end');
// main.js
const author = require('./author.js');
const book = require('./book.js');
console.log('main:', author.ready, book.ready);
author: start
book: start
book: author.ready is false
book: end
author: book.ready is true
author: end
main: true true
The cycle terminates because book sees author.ready === false. The shape that really breaks is replacing
module.exports in a module that takes part in a cycle, because the other side keeps the object it received earlier:
module.exports inside a cycle// trap-a.js
const b = require('./trap-b.js');
module.exports = { name: 'a', partner: b.name }; // a replaces its exports object after b already got the old one
// trap-b.js
const a = require('./trap-a.js'); // b receives a's half-finished, still empty exports object
module.exports = { name: 'b', keysSeenOfA: Object.keys(a) };
// trap.js
console.log(require('./trap-a.js'), require('./trap-b.js'));
{ name: 'a', partner: 'b' } { name: 'b', keysSeenOfA: [] }
Fixes, in order of preference: remove the cycle by moving the shared code to a third module; require lazily inside the function
that needs the other module; or export with exports.x = … only and never reassign module.exports in modules that take
part in a cycle. On Node.js 26 and 22, reading a property that does not exist on a half-finished exports object during a cycle also printed a
warning that names "circular dependency" (observed while experimenting; it is not described on the modules page), which is a
useful clue when a value is mysteriously undefined. ES modules handle cycles differently, through live bindings, as covered in
ECMAScript Modules.
The entry point: require.main
When a file is run directly with node file.js, require.main is that file’s module, so require.main === module is the
classic "am I the program?" test. It is true when run directly and false when the file is loaded with require(). When the
entry point is not a CommonJS module (for instance an .mjs file), require.main is undefined.
// tool.js
function main() { console.log('running as the entry point'); }
if (require.main === module) main();
else console.log('loaded by', require('node:path').basename(module.parent?.filename ?? 'unknown'));
module.exports = { main };
// user.js
require('./tool.js');
node tool.js and node user.js on Node.js 26, 24 and 22running as the entry point
loaded by user.js
The else branch above uses module.parent only to make the example visible. It is deprecated (DEP0144, documentation-only; with
--pending-deprecation Node.js 26 printed module.parent is deprecated due to accuracy issues) because it is inaccurate in the
presence of ES modules. To find the modules that required the current one, filter require.cache for entries whose children
include module. process.mainModule (object in a CommonJS entry point) is the older alias of require.main and is
deprecated for the same reasons (DEP0138). ES modules have no require.main; use import.meta.main, added in v24.2.0 and v22.18.0.
Loading JSON and native add-ons
Three file types go through require(): .js (and .cjs) files are compiled as JavaScript, .json files are parsed as JSON text,
and .node files are loaded as compiled add-ons with process.dlopen(). Any other extension is treated as JavaScript text.
Object.keys(require.extensions) printed [ '.js', '.json', '.node' ] on all three lines.
// settings.json: { "shelves": 4, "owner": "Bookshelf" }
const settings = require('./settings.json'); // parsed with JSON.parse, cached like any module
settings.shelves += 1; // mutates the cached object
console.log('settings:', settings, '| same object next time:', require('./settings') === settings);
console.log('extensions handled:', Object.keys(require.extensions));
settings: { shelves: 5, owner: 'Bookshelf' } | same object next time: true
extensions handled: [ '.js', '.json', '.node' ]
Because the parsed object is cached, mutating it changes it for every other module in the process; copy it with structuredClone()
if you need a private version. require('./settings') finds settings.json only because the extension search tries .json
after .js; writing the extension is faster and clearer. require.extensions can register more handlers but is deprecated
(Stability 0): the documentation recommends compiling ahead of time, or running a loader outside the process, instead; each
registered extension also slows resolution.
A native add-on is a shared library that exposes Node-API (Stability 2 Stable, an ABI that stays stable across Node.js
versions, so one build works on several lines). The next example is a complete add-on in C, compiled by hand against the
headers that ship with Node.js; for real projects use node-gyp or CMake.js as described in the Learn guide.
// hello.c
#include <node_api.h>
static napi_value Hello(napi_env env, napi_callback_info info) {
napi_value result;
napi_create_string_utf8(env, "hello from a native add-on", NAPI_AUTO_LENGTH, &result);
return result;
}
NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {
napi_value fn;
napi_create_function(env, "hello", NAPI_AUTO_LENGTH, Hello, NULL, &fn);
napi_set_named_property(env, exports, "hello", fn);
return exports;
}
$ cc -shared -undefined dynamic_lookup -I"$(dirname "$(dirname "$(which node)")")/include/node" -o hello.node hello.c
$ node -e "const addon = require('./hello.node'); console.log(addon.hello(), '|', Object.keys(require.cache).some((k) => k.endsWith('hello.node')))"
hello from a native add-on | true
The same hello.node file loaded on Node.js 26, 24 and 22. Linux needs -fPIC and no -undefined dynamic_lookup, and Windows
needs a different toolchain (these two were not run here). A .node file that is not a valid shared library fails with the
error code ERR_DLOPEN_FAILED (measured with a text file renamed to fake.node). Loading an add-on a second time after deleting its
cache entry is an error, as noted in the documentation, so the cache of add-ons cannot be reset.
Importing a .node file from an ES module needs --experimental-addon-modules, which is Stability 1.2 Release candidate. It
became enabled by default in v24.19.0 and v26.5.0 according to each line’s CLI page; on Node.js 22.23.3 the unflagged import()
failed with ERR_UNKNOWN_FILE_EXTENSION and succeeded with the flag, and on 24.21.0 and 26.10.0 it worked without the flag
and printed an ExperimentalWarning. process.dlopen() is the underlying call; see the
addons documentation and
the Learn article on ABI stability for the reasons to prefer Node-API.
Folders as modules (Legacy)
Passing a directory to require() works in three ways, marked Stability 3 Legacy in the documentation ("Use subpath exports or
subpath imports instead"):
-
the directory contains a
package.jsonwhose"main"points to the entry file; -
there is no
package.json, or"main"is missing or cannot be resolved, so Node.js looks forindex.jsand thenindex.node(the algorithm also listsindex.json); -
if all of that fails, the error is
Cannot find module.
// inventory/package.json: { "name": "inventory", "main": "./src/inventory.js" }
// byindex/index.js, and nomain/package.json with { "main": "./missing.js" } plus nomain/index.js
for (const dir of ['./inventory', './byindex', './nomain']) console.log(dir, '->', require(dir));
./inventory -> inventory via package.json main
./byindex -> byindex via index.js
./nomain -> nomain falls back to index.js
(node:21366) [DEP0128] DeprecationWarning: Invalid 'main' field in '.../nomain/package.json' of './missing.js'. Please either fix that or report it to the module author
The third line shows the fallback that DEP0128 deprecates: a package with an invalid "main" that also has an index.js
still loads, with a runtime deprecation warning, and a future Node.js version is expected to throw. ES modules never do any of
this: await import('./byindex') failed with ERR_UNSUPPORTED_DIR_IMPORT on all three lines. For code that must work with
both loaders, declare entry points with "exports" and internal aliases with "imports".
Loading ES modules with require()
For years the rule was "`require()` cannot load an ES module". That changed: require() can now load an ES module graph that
is fully synchronous, meaning it contains no top-level await. The ES module is eligible when the file is .mjs, or .js under
"type": "module", or .js containing ES syntax in a package that does not say "type": "commonjs". The result is the module
namespace object, so a default export is under .default; a namespace that has a default export also gets __esModule: true
(the documentation marks that property as experimental and meant for tooling, and says hand-written code should not depend on it).
An ES module can instead export the special name 'module.exports' to decide exactly what require() returns.
'module.exports' export name, and a top-level await graph// isbn.mjs
export function check(s) { return /^\d{13}$/.test(s); }
export default class Isbn { constructor(v) { this.value = v; } }
// price.mjs
export function format(cents) { return `$${(cents / 100).toFixed(2)}`; }
export { format as 'module.exports' };
// later.mjs
await Promise.resolve();
export const ready = true;
// load.cjs
console.log('process.features.require_module:', process.features.require_module);
const isbn = require('./isbn.mjs');
console.log(isbn);
console.log('default is the class:', typeof isbn.default, '| check:', isbn.check('9781098145187'));
console.log("'module.exports' export:", require('./price.mjs')(1999));
try { require('./later.mjs'); } catch (err) { console.log(err.code); }
import('./later.mjs').then((ns) => console.log('import() works:', ns.ready));
process.features.require_module: true
[Module: null prototype] {
__esModule: true,
check: [Function: check],
default: [class Isbn]
}
default is the class: function | check: true
'module.exports' export: $19.99
ERR_REQUIRE_ASYNC_MODULE
import() works: true
process.features.require_module detects the feature; --no-require-module turns it off (then the same code throws
ERR_REQUIRE_ESM; the old name --no-experimental-require-module was also accepted on Node.js 26), and --trace-require-module=all
prints where it is used (no-node-modules leaves out usage inside node_modules). --experimental-print-required-tla
locates the top-level await when ERR_REQUIRE_ASYNC_MODULE is thrown; since v26.5.0 it prints the locations without
evaluating the modules. The full interoperability story (named exports from CommonJS, createRequire, hooks, migration) is in
ESM & CommonJS Interop.
Errors you will see
error.code |
Typical cause | What to check |
|---|---|---|
|
Nothing matched the specifier anywhere in the resolution procedure. |
Spelling and relative path; the package is installed ( |
|
The package has an |
The package’s |
|
|
Use |
|
|
Remove the flag or use |
|
|
Import the file, or use |
|
A |
Rebuild the add-on for the current platform; prefer Node-API. |
What changed recently
| Version | Change |
|---|---|
v26.5.0 |
|
v26.4.0 |
Package maps ( |
v25.4.0 |
|
v24.2.0, v22.18.0 |
|
v23.5.0 |
|
v23.0.0, v22.12.0, v20.19.0 |
|
v22.7.0, v20.19.0 |
Syntax detection is on by default, so a |
v16.0.0, v14.18.0 |
|
v14.6.0, v12.19.0 |
|
Where the books are outdated
Efficient Node.js (Samer Buna, O’Reilly 2025) covers CommonJS next to ES modules in its chapter on scripts and modules:
the five-parameter scope, require.resolve(), module.exports, require.main and caching. Most of that is still correct. Four
points need updating. It says that ES modules have no simple way to check whether a file is the program and points to a
third-party package plus import.meta.url; Node.js now has import.meta.main (v24.2.0 and v22.18.0), as the table above shows. It
describes NODE_PATH as consulted first and as a comma-separated list; it is consulted after the node_modules walk and is
separated by : on Unix and ; on Windows (both behaviours measured above), and it is ignored by ES modules (the ES module
page says NODE_PATH is not part of resolving import specifiers; a import('tag') that worked through NODE_PATH in CommonJS
failed with ERR_MODULE_NOT_FOUND). At least one listing uses exports in an ES module, where it does not exist. And the book
does not mention loading an ES module with require(): since v22.12.0, v20.19.0 and v23.0.0 without a flag, and stable in
v25.4.0, a synchronous ES module graph can be required, which removes the main reason to keep dual builds. Node.js Projects
(Jonathan Wexler, O’Reilly 2025) mentions CommonJS only in passing, in a remark that developers now prefer ES modules, and its
sample code sets "type": "module"; for CommonJS details use the official page.
The advice in older tutorials to hot-reload by clearing require.cache, to treat module.parent as the entry-point test, to
register custom loaders through require.extensions, and to shorten paths with NODE_PATH should be replaced by node --watch,
require.main === module (or import.meta.main), ahead-of-time compilation, and "imports" aliases respectively.
References
-
Node.js API: Modules: CommonJS modules, https://nodejs.org/docs/latest/api/modules.html
-
Node.js API: Modules: node:module API (
module.builtinModules,module.createRequire()), https://nodejs.org/docs/latest/api/module.html -
Node.js API: Modules: Packages (
"exports","imports", package maps), https://nodejs.org/docs/latest/api/packages.html -
Node.js API: Modules: ECMAScript modules, https://nodejs.org/docs/latest/api/esm.html
-
Node.js API: Command-line options (
--no-require-module,--trace-require-module,--experimental-print-required-tla,--experimental-addon-modules,--preserve-symlinks,NODE_PATH), https://nodejs.org/docs/latest/api/cli.html -
Node.js API: Deprecated APIs (DEP0128, DEP0138, DEP0144), https://nodejs.org/docs/latest/api/deprecations.html
-
Node.js API: Process (
process.dlopen()), https://nodejs.org/docs/latest/api/process.html -
Node.js API: C/C++ addons with Node-API, https://nodejs.org/docs/latest/api/n-api.html
-
Node.js API: C++ addons, https://nodejs.org/docs/latest/api/addons.html
-
Node.js Learn: Node-API build tools (node-gyp), https://nodejs.org/learn/node-api/build-tools/node-gyp
-
Node.js Learn: ABI stability, https://nodejs.org/learn/modules/abi-stability
-
O’Reilly: Efficient Node.js, Samer Buna, https://www.oreilly.com/library/view/efficient-node-js/9781098145187
-
O’Reilly: Node.js Projects, Jonathan Wexler, https://www.oreilly.com/library/view/node-js-projects/9781098173135/