Understanding CommonJS and ES modules, module resolution, require() internals, and the dual module system in Node.js
Node.js supports two module systems: the original CommonJS format and the ECMAScript modules (ESM) standard. Understanding how both work and interact is essential for modern Node.js development.
(function (exports, require, module, __filename, __dirname) { // Your module code here const foo = 'bar'; module.exports = foo;});
The wrapper function provides the exports, require, module, __filename, and __dirname variables that appear to be global but are actually local to each module.
The require() function is the heart of CommonJS module loading:
// From lib/internal/modules/cjs/loader.jsModule.prototype.require = function(id) { validateString(id, 'id'); if (id === '') { throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string'); } return Module._load(id, this, /* isMain */ false);};
First, check if the module is already loaded in Module._cache.
2
Check Native Modules
If it’s a built-in module like ‘fs’ or ‘http’, load it directly.
3
Create New Module
If not cached, create a new Module instance and add it to the cache.
4
Load Module Content
Read and compile the module file, wrapping it in the module wrapper.
5
Execute Module
Run the wrapped function, providing the module context.
6
Return Exports
Return the module.exports object to the caller.
// From lib/internal/modules/cjs/loader.js - simplifiedModule._load = function(request, parent, isMain) { // 1. Resolve filename const filename = Module._resolveFilename(request, parent, isMain); // 2. Check cache const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { return cachedModule.exports; } // 3. Check if native module const mod = loadBuiltinModule(filename, request); if (mod?.canBeRequiredByUsers) return mod.exports; // 4. Create new module const module = new Module(filename, parent); // 5. Add to cache before loading (for circular dependencies) Module._cache[filename] = module; // 6. Load the module module.load(filename); // 7. Return exports return module.exports;};
Modules are cached after the first load. Subsequent require() calls for the same module return the cached exports object, not re-executing the module code.
CommonJS handles circular dependencies by returning partially-filled exports:
// a.jsconsole.log('a starting');exports.done = false;const b = require('./b.js');console.log('in a, b.done =', b.done);exports.done = true;console.log('a done');// b.jsconsole.log('b starting');exports.done = false;const a = require('./a.js');console.log('in b, a.done =', a.done);exports.done = true;console.log('b done');// main.jsconst a = require('./a.js');const b = require('./b.js');// Output:// a starting// b starting// in b, a.done = false ← a is not finished!// b done// in a, b.done = true// a done
When module A requires module B, and B requires A, Node.js returns the incomplete exports object from A to break the cycle. The module is already in the cache, preventing infinite recursion.
ESM loading is asynchronous because it may need to fetch modules from the network (in browsers) or perform other async operations. Node.js maintains this behavior for consistency with the standard.
ES modules can import CommonJS modules, but only the default export:
// cjs-module.js (CommonJS)module.exports = { foo: 'bar', baz: 42};// esm-module.mjs (ES Module)import cjsModule from './cjs-module.js';console.log(cjsModule.foo); // 'bar'// Named imports from CJS require special handlingimport { createRequire } from 'module';const require = createRequire(import.meta.url);const cjs = require('./cjs-module.js');
Importing ESM from CommonJS
CommonJS cannot use synchronous require() for ESM. Use dynamic import:
// This does NOT workconst esm = require('./esm-module.mjs'); // Error// Use dynamic import insteadasync function loadESM() { const esm = await import('./esm-module.mjs'); console.log(esm.default);}
You cannot require() an ES module. The require() function is synchronous, but ESM loading is asynchronous. Always use dynamic import() to load ESM from CommonJS.
// From lib/internal/modules/cjs/loader.jsModule._cache = ObjectCreate(null);// You can access and manipulate itconsole.log(require.cache);// Clear a specific module from cachedelete require.cache[require.resolve('./my-module.js')];
import debounce from 'lodash/debounce.js';// With "exports" in package.json:{ "exports": { "./debounce.js": "./debounce.js" }}
The “exports” field in package.json allows package authors to define which files can be imported, providing encapsulation and preventing users from accessing internal implementation details.