Skip to content
Latchkey

Node ERR_REQUIRE_ESM "require() of ES Module" in CI - Fix the Interop

ERR_REQUIRE_ESM means CommonJS code called require() on a package that only ships an ES module. On older Node, require cannot load ESM synchronously.

What this error means

A node script throws Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported, usually after upgrading a dependency that went ESM-only.

node
Error [ERR_REQUIRE_ESM]: require() of ES Module
/home/runner/work/app/app/node_modules/chalk/source/index.js
from /home/runner/work/app/app/src/log.js not supported.
Instead change the require of index.js to a dynamic import().

Diagnose it: the shell in CI is not your shell

Package scripts run under a different shell, a different PATH, and a non-interactive environment on a runner. Most scripts that fail only in CI are relying on something the login shell gave them locally: a tool on PATH, an environment variable from a dotfile, or a TTY.

Terminal
# what the script can actually see
npm run env | grep -E "^(PATH|NODE_ENV|CI)=" 

# is the binary on PATH for the script, not just for you?
npm exec -- which <tool> || echo "not resolvable from npm scripts"

# run the exact script with tracing
sh -x -c "$(node -p "require('./package.json').scripts.build")"

Common causes

A dependency dropped CommonJS support

The package now publishes ESM only, so a require() of it can no longer load synchronously on Node versions before the require(esm) support.

A CommonJS caller importing an ESM-only module

Your entry point is CommonJS but it pulls in a module that exposes no CommonJS entry.

How to fix it

Use a dynamic import

  1. Replace the require() with an await import() inside an async function.
  2. Read the default export off the resolved module namespace.
JavaScript
const { default: chalk } = await import('chalk');

Pin a CommonJS-compatible version

  1. If converting to ESM is not feasible, pin the last release that shipped CommonJS.
  2. Schedule the ESM migration separately.
Terminal
npm install chalk@4

Make failures fail the job

A multi-command script can report success while a middle command failed, which produces the worst kind of CI result: a green build that shipped something broken.

.github/workflows/ci.yml
# pipefail is NOT set by default in every runner shell
- name: Build
  shell: bash
  run: |
    set -euo pipefail
    npm run build | tee build.log

How to prevent it

  • Track which dependencies have gone ESM-only, plan a deliberate move of the consuming package to ESM, and avoid mixing synchronous require with ESM-only modules.

Frequently asked questions

What causes Node ERR_REQUIRE_ESM "require() of ES Module" in CI?
There are 2 common causes: a dependency dropped commonjs support and a commonjs caller importing an esm-only module. The package now publishes ESM only, so a require() of it can no longer load synchronously on Node versions before the require(esm) support.
How do I fix Node ERR_REQUIRE_ESM "require() of ES Module" in CI?
There are 2 fixes depending on which cause you have: use a dynamic import and pin a commonjs-compatible version. Work through them in order, since the first is the most common.
What does Node ERR_REQUIRE_ESM "require() of ES Module" in CI actually mean?
A node script throws Error [ERR_REQUIRE_ESM]: require() of ES Module ...
How do I stop Node ERR_REQUIRE_ESM "require() of ES Module" in CI happening again?
Track which dependencies have gone ESM-only, plan a deliberate move of the consuming package to ESM, and avoid mixing synchronous require with ESM-only modules.

Related guides

References

Not every red build is your code. Latchkey repairs the ones that are not, on the runner. Start free → 30-day trial · No credit card