CommonJS and ESM
How to investigate ERR_REQUIRE_ESM
Understand why require() reaches an ES module, how Node versions change the result, and when to use import() or migrate to ESM.
The core answer
ERR_REQUIRE_ESM means a CommonJS require() call reached an ES module that the running Node environment could not load through that path. Check the exact Node version, resolved package version, and selected export before choosing a fix. A package declaring type: module is a signal to investigate, rather than proof that every CommonJS consumer will fail.
The Node version changes the diagnosis
Node introduced require(ESM) in 20.17.0 and 22.0.0. It no longer requires the experimental CLI flag in Node 20.19.0, 22.12.0, and 23.0.0 onward within those release lines. Earlier releases have different support and flag requirements.
This support applies only to a fully synchronous ES module graph. Top-level await in the module or a dependency can produce ERR_REQUIRE_ASYNC_MODULE. Loading an ESM namespace also does not guarantee that existing code can keep calling the returned value like a CommonJS default export.
Choose an import boundary deliberately
A CommonJS file can use dynamic import() inside an async function. This keeps the caller in CommonJS while introducing an asynchronous boundary. Check the package documentation for its actual named and default exports.
Migrating a project to ESM affects more than one import statement: file extensions, package type, relative specifiers, test tooling, and configuration files may all need review. If migration is not practical, evaluate a documented alternative release against its support and security status before pinning it.
async function main() {
const packageModule = await import('example-package');
// Use the documented exports of this package.
console.log(Object.keys(packageModule));
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});example-package is a placeholder. This demonstrates an async import boundary; it is not a tested migration for a published package.
What to check next
- 1
Record the environment that actually fails
Run node --version in the failing process environment and inspect the stack trace. CI, containers, test runners, and your terminal can use different Node installations.
- 2
Identify the resolved dependency and entry point
Inspect the installed version and its package.json. Check exports, type, and the importing file. A transitive dependency can introduce the require() call even when your source already uses import.
- 3
Compare fixes, then run the affected path
Evaluate a supported Node upgrade, a documented import() boundary, or an ESM migration. Run your tests and the failing command with the intended lockfile and deployment runtime.
What a declaration cannot establish
- Manifest fields cannot reveal every top-level await, transitive module, or runtime flag.
- A successful declaration check does not test a package's runtime behavior or the shape of its exports.
- Bundlers and transpilers can select or transform modules differently from native Node resolution.