Load ES Modules with require in Node.js
This article explains how newer versions of Node.js allow developers
to natively load ECMAScript Modules (ESM) using the traditional CommonJS
require() function. We will cover the mechanics behind this
feature, the specific conditions under which it operates, and how it
simplifies dependency management in modern JavaScript environments.
The Historical Interoperability Barrier
Historically, Node.js maintained a strict barrier between CommonJS
(CJS) and ES Modules (ESM). CJS modules relied on the synchronous
require() function, while ESM used the asynchronous
import statement.
Because ESM execution is asynchronous by design, loading an ES module
from a CommonJS file was highly restricted. CommonJS developers were
forced to use asynchronous dynamic import() expressions to
load ESM dependencies. This created significant friction, as developers
had to rewrite synchronous code paths to be asynchronous just to consume
an ES module.
Synchronous ESM Loading via require()
To resolve this ecosystem divide, Node.js introduced support for
synchronously loading ES modules using the standard
require() function.
When you pass an ESM file path to require(), Node.js
synchronously compiles and executes the target ES module. It then
returns the module’s namespace object, with default exports mapped to
the .default property and named exports mapped to their
respective keys.
The Top-Level Await Constraint
For require() to successfully load an ES module, the
target module must meet one critical condition: it must not
contain top-level await statements.
Because require() is fundamentally synchronous, it
cannot pause its execution thread to wait for a promise to resolve. If
Node.js encounters a top-level await in the required ES
module or any of its deep dependencies, it will immediately throw an
ERR_REQUIRE_ESM error. If an ES module contains top-level
await, it must still be loaded using dynamic
import().
Version Support and Usage
This feature was first introduced in Node.js v20.12.0 and Node.js v22.0.0 under the command-line flag:
--experimental-require-module
Starting with Node.js v22.12.0, this behavior is enabled by default. You no longer need to pass any experimental flags to require compatible ES modules in your CommonJS code.
Example Code
Consider a simple ES module:
// math.mjs (ES Module)
export function add(a, b) {
return a + b;
}You can now require this module directly inside a CommonJS file:
// index.cjs (CommonJS Module)
const { add } = require('./math.mjs');
console.log(add(5, 10)); // Outputs: 15Why This Matters
This update greatly simplifies package maintenance across the JavaScript ecosystem. Library authors no longer need to bundle their code into dual CJS/ESM distribution formats to support both environments. CommonJS applications can seamlessly adopt newer ESM-only libraries without undergoing a complete architectural rewrite.