← Back to the library
FoundationsApplied · 7 min

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 · platform

require() 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 reviewed
ERR_REQUIRE_ASYNC_MODULENode.js errors · platform

require() 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

  1. 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.
  2. Determine each file's format: .mjs/.cjs extensions, the nearest package.json "type", and the package's "exports" conditions (import vs require).
  3. 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.
  4. "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.
  5. 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

Pinning old CJS versions or await import() everywhere to avoid ERR_REQUIRE_ESMrequire(esm) on Node 20.19+/22.12+ for synchronous ES modules; import() for ones with top-level await
esModuleInterop and trial and errorTypeScript module: nodenext so type checking matches Node's actual interop

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?

SOURCES

Guide reviewed