Trust is earned, not given

A different perspective

2024-01-16 · Projects

Node.js, part 22: require(esm) — the dual-package problem finally closes

Part 22from the Node.js series · 24 parts in all

Part 5 left an asymmetry standing: a module could import CommonJS, but CommonJS could not require an ES module. Every library author paid for it, usually by shipping two builds, importing two packages, or refusing to migrate. Node 22 made require(esm) work. Part 22 is what that fixes and the one case that still fails.

Requiring a module, at last

// lib.mjs  (an ES module, maybe in a dependency you do not control)
export function parse(text) { /* ... */ }
export const VERSION = '2.1.0';

// consumer.cjs
const { parse, VERSION } = require('./lib.mjs');   // Node 22+, previously ERR_REQUIRE_ESM

It works without a flag in Node 22.12 and later, and it is the feature that lets a large CommonJS codebase adopt far more of the ecosystem without a top-to-bottom migration.

The one restriction: top-level await

Requiring a module whose initialisation is asynchronous cannot work synchronously — there is no value to return yet. So a module with a top-level await (part 12) still refuses to be required, and the error is explicit about why. The fix is await import('./lib.mjs'), which is asynchronous by construction and therefore always legal:

// Only option when the module awaits during initialisation.
const { parse } = await import('./lib.mjs');

Why this mattered so much more than it looks

The residual caveat is the load graph, not the API: if the same package is pulled in as both CJS and ESM you can still get two instances, so a stateful library is better published as one format with a default export that both worlds can consume. Next: everything else in Node 22.