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.
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 -30Interpretation
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.