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.

Based on official documentationUpdated

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.

Illustrative CommonJS consumer · app.cjs
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. 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. 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. 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.

Official references

From explanation to inspection

Check an exact package release.

Compare a package's declared Node requirements and root entry point with your environment, then verify it in your application.

Prefilled example: chalk 5.6.2 · Node 20.0.0 · CommonJS. Run the checker to retrieve the declaration; this guide does not claim an observed result.

Inspect the example
All guides