Node.js, part 5: ES modules arrive — import, export, and the interop rules
Part 5from the Node.js series · 24 parts in all
For years require() was the only way to load code in Node, and it was
genuinely different from the import the browser wanted. Node 12 made ES modules
work without a flag. Part 5 is the two systems, how they interoperate, and the small set of
facts that explains every confusing error you will meet.
Opting a file in
The file extension is the switch: .mjs is always a module, .cjs
is always CommonJS, and a plain .js follows the nearest
package.json:
{
"name": "my-service",
"type": "module"
}
// now every .js in the package is an ES module
import { readFile } from 'node:fs/promises';
import path from 'node:path'; // default import of a CJS module
export function load() { /* ... */ }
export default load;
The differences that actually bite
- Imports are hoisted and static. An
importcannot be inside anif; that is what the dynamicawait import('./plugin.js')is for. - No
__dirname, norequire, nomodule.exports. They do not exist in module scope. The bridge is explicit:import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const __dirname = path.dirname(fileURLToPath(import.meta.url)); - Cycles behave differently. CommonJS hands back a partially filled
exportsobject; ESM resolves bindings live, so a cycle can now seeundefinedwhere CJS would have seen a stale value — or the opposite. Either way, when a cycle appears, the fix is to remove the cycle.
Interop, stated once
A module can import a CommonJS package, and the default export is
module.exports. CommonJS could not require() an ES module
for most of this period — that asymmetry is what made the dual-package problem so annoying,
and it is exactly what part 22 fixes.
One practical rule for libraries: shipping both builds means the two copies can be loaded
into one process, and a instanceof check across the boundary fails. Keep shared
state in a single module, or better, one build. Next: the EventEmitter, Node's original
concurrency primitive.