← Debugging Guides

阅读中文版本

Node.js — ERR_REQUIRE_ESM When require() Meets an ESM-Only Package

Resolve ERR_REQUIRE_ESM by checking the package exports field and type, then migrate the importer to dynamic import, ESM syntax, or a CJS-compatible version.

intermediatedebuggingnodejsesmmodules

Symptoms

  • Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported, with a path pointing into node_modules
  • The failure names a package that recently published a v3 with an ESM-only build (chalk, node-fetch, execa family)
  • The importing file itself is CommonJS (uses require and module.exports)

Possible causes

  • The dependency now ships only ES modules and declares type: module with exports restricting require access
  • The lockfile advanced to the new major version after a range like ^2 or a caret-refresh, without a code change
  • A nested transitive dependency still on the old major is fine, but the direct import pulled the new one
  • A tool loads a config file with require and the config imports an ESM-only helper

Diagnosis steps

Read the package entry policy

node -p "require('./node_modules/execa/package.json').type + ' | exports: ' + JSON.stringify(require('./node_modules/execa/package.json').exports)",

Interpretation

type: module with an exports map lacking a require condition guarantees that require() cannot load it, by design.

Check who advanced the version

git log -p package-lock.json | grep -B2 -A2 '"version": "3' | head -30

Interpretation

A lockfile diff that moved the package across a major boundary explains why working code stopped loading.

Classify the importing file

head -5 src/legacy.js | grep -E "require\(|import "

Interpretation

The loader choice of the importing file determines which fixes are available: ESM syntax, dynamic import, or staying on a CJS build.

Root cause

The dependency now publishes only ES modules. CommonJS require() loads synchronously and returns a namespaced module object, which is incompatible with ESM linking semantics, so Node refuses the load with ERR_REQUIRE_ESM rather than silently returning a broken module.

Fix

Choose one path per importer: migrate the importing file to ESM (type module plus import syntax), use const mod = await import('pkg') for lazy loading inside CJS, or pin the last dual-mode major of the dependency while planning the migration. Keep mixed codebases consistent with .mjs and .cjs extensions where needed.

Risk note

Pinning the old major forever forfeits upstream fixes and security patches. Treat the pin as a bridge with a dated migration task.

Verify the fix

Run the affected entry points after migration and assert successful loading plus correct named exports; run the whole test suite to catch default-versus-named export differences.

Symptom walkthrough

The error text carries the verdict and the instruction:

Error [ERR_REQUIRE_ESM]: require() of ES Module
/app/node_modules/chalk/source/index.js not supported.
Instead change the requiring code to use import()

Inspect the policy that produced it:

node -p "require('./node_modules/chalk/package.json').type"
# module

type: module plus an exports map without a require condition means the package author closed the require() door deliberately. The trigger is almost always a version bump: the lockfile advanced to the ESM-only major, and the require call in a CommonJS file is now structurally unsupported.

Fix in practice

Three migration shapes, pick per file. Dynamic import keeps the file CommonJS:

async function run() {
  const { default: chalk } = await import('chalk');
  console.log(chalk.green('done'));
}

Full ESM migration changes the file and the package type:

{ "type": "module" }
import chalk from 'chalk';
export function ok(msg) { console.log(chalk.green(msg)); }

When migration must wait, pin the last dual-mode release explicitly:

{ "dependencies": { "chalk": "^4.1.2" } }

On Node 22.12+, require(esm) works natively, which can retire this entire error class — but verify named-export interop before relying on it, because default versus named exports still behave differently across the boundary.

Verification

Boot every entry point that previously failed (server, jobs, scripts) and assert clean loading. Then run the full test suite with special attention to import shapes: replace default x with destructured { x } where the package exports only named bindings, and vice versa. Finally, grep the codebase for remaining require() calls into the migrated packages and confirm each is either converted or intentionally pinned, so the next lockfile refresh does not reintroduce ERR_REQUIRE_ESM.