A dependency upgrade breaks module loading
“After bumping a package, Node fails with ERR_REQUIRE_ESM or "Cannot use import statement outside a module", or an import works in the bundler but at runtime x.default is the function and x() is "not a function".”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
ERR_REQUIRE_ESMNode.js errors · platformrequire() was called on an ES module. Deprecated: Node.js 20.19+, 22.12+ and 23+ can require() synchronous ES modules by default.
Seeing it usually means an older Node.js runtime or --no-require-module. Check `node --version` in the failing environment before changing package code.
Official reference for ERR_REQUIRE_ESM (opens in new tab)Code definition reviewedERR_REQUIRE_ASYNC_MODULENode.js errors · platformrequire() reached an ES module whose graph uses top-level await, which cannot be loaded synchronously.
Run with --experimental-print-required-tla to locate the top-level await, then load that module with import() instead.
Official reference for ERR_REQUIRE_ASYNC_MODULE (opens in new tab)Code definition reviewed
THE PRINCIPLE
Know which loader runs each file (Node, bundler, test runner, TS compiler) and what it thinks the file's format is; most interop bugs are two tools disagreeing.
FIRST MOVES
- Check node --version where it fails: require(esm) is on by default from 20.19 and 22.12; process.features.require_module tells you at runtime.
- Determine each file's format: .mjs/.cjs extensions, the nearest package.json "type", and the package's "exports" conditions (import vs require).
- ERR_REQUIRE_ASYNC_MODULE means top-level await somewhere in the graph: use await import() for that module, and --experimental-print-required-tla to find it.
- "Cannot use import statement outside a module" usually comes from a tool loading the file as CommonJS (Jest's default runtime, an old ts-node, "type": "commonjs"): configure that tool for ESM rather than rewriting the code.
- For default-import mismatches, remember Node gives ESM the whole module.exports as default and ignores __esModule; set TypeScript module to nodenext so the compiler models the same rules.
TOOLS: THEN → NOW
PATTERN SNAPSHOT
// CommonJS requiring an ESM-only package (Node 22.12+, 24, 26):
const { slugify } = require("esm-only-pkg"); // works if its graph has no top-level await
const { default: Client } = require("./client.mjs"); // require(esm) returns the namespace
// Top-level await in the graph: ERR_REQUIRE_ASYNC_MODULE. Load it asynchronously.
async function loadTelemetry() {
const { init } = await import("tla-pkg");
return init();
}
// ESM importing CommonJS: the default import is module.exports.
import legacy from "./legacy.cjs";
const handler = legacy.default ?? legacy; // transpiled CJS with __esModule: "double default"CLOSE THE AI. EXPLAIN THIS.
On Node 24, what does require() return for an ES module with a default export, and what still makes require(esm) fail?RELATED SYMPTOMS
SOURCES
- Node.js modules: Loading ECMAScript modules using require() (opens in new tab)Checked
- Node.js errors: ERR_REQUIRE_ESM and ERR_REQUIRE_ASYNC_MODULE (opens in new tab)Checked
- TypeScript: ESM/CJS interoperability (opens in new tab)Checked
- Node.js: Previous releases (current LTS lines) (opens in new tab)Checked
Guide reviewed