Technology · PRACTICAL GUIDE
Node says ERR_REQUIRE_ESM? Trace the runtime caller before changing module settings
The source may say import while the emitted JavaScript, a dependency, or a test runner still calls require(). Prove the runtime boundary, then choose a full ESM migration, a dynamic-import bridge, or a reviewed temporary version pin.

ERR_REQUIRE_ESM identifies a module-format boundary at runtime. The line you wrote may not be the line Node executed: TypeScript, Babel, Vite, Electron, Jest or a transitive dependency can still produce or call require().
Preserve the first stack trace, record the exact Node version and inspect the executed JavaScript before editing package.json. The repair depends on who owns the CommonJS caller and whether the target ESM graph is synchronously loadable.
Freeze the runtime evidence first
Save the complete stack, command, working directory, Node version, package-manager version and lockfile revision. Mark the target ESM file and the first caller outside Node internals.
Do not begin by deleting node_modules or the lockfile. Those actions can change the resolved package graph and erase the exact boundary that failed.
Inspect what actually executed
Open the caller path named in the stack. If it is under dist, out, build or an Electron bundle, check whether the emitted line is require() even when the source used import.
For TypeScript, record module, moduleResolution, the caller extension and the nearest package.json type field. node16, node18 and nodenext model per-file Node behavior instead of forcing one global format.
Map the package boundary and dependency owner
Use the lockfile and dependency tree to identify the exact package version and whether your code, a direct dependency or a transitive dependency owns the CommonJS call. Read that package manifest exports and type fields.
Also record the active Node version. A version-specific resolver regression or a top-level-await graph requires a different repair from an ordinary ESM-only package upgrade.
Choose one repair path
If your application can migrate coherently, change the package boundary, compiler output, relative extensions and runtime entry together, then test every entry point. If CommonJS must remain, use dynamic import at the owned async boundary when the API can become asynchronous.
When the failing require is inside a dependency, prefer an upstream release that supports your runtime. A reviewed temporary pin can restore service when the package author still supports that branch, but record the reason, security exposure and removal date.
Verify the fix at the same boundary
Rebuild from a clean output directory without rewriting the lockfile unexpectedly. Confirm the executed caller no longer emits require for the ESM-only target, or that the dynamic-import bridge returns the expected default or named export.
Run the original command, tests and production entry with the recorded Node version. Save the final dependency tree and emitted snippet so a future upgrade can distinguish a package-format change from a runtime regression.
