Fix: Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
Part of: JavaScript & TypeScript Errors
Quick Answer
Fix Node.js 'Error [ERR_REQUIRE_ESM]: require() of ES Module not supported' by checking your Node version, using dynamic import(), or converting to ESM.
I got bitten by this the week chalk went ESM-only and half my CI pipeline turned red overnight. Nothing in my code had changed, a dependency’s dependency had shipped a major version that dropped CommonJS support, and require() refused to load it. In my experience the fastest path through this error is to first check your Node version, because Node quietly fixed the common case in late 2024 and early 2025, and a lot of the advice still floating around online is written for Node versions where that fix did not exist yet. This covers what changed, why it still bites you even on a current Node, and every fix in order of how little you should have to change.
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
Your Node.js process throws this when it loads:
Error [ERR_REQUIRE_ESM]: require() of ES Module /project/node_modules/chalk/source/index.js not supported.
Instead change the require of index.js to a dynamic import() which is available in all CommonJS modules.
at Module._extensions..js (node:internal/modules/cjs/loader:1355:11)
at Module.load (node:internal/modules/cjs/loader:1189:32)The line that triggers it looks completely ordinary:
const chalk = require('chalk'); // chalk v5+ is ESM-onlyBundlers and test runners wrap the same failure in their own message. Jest:
Jest encountered an unexpected token
SyntaxError: Cannot use import statement outside a moduleNext.js, when an ESM-only dependency reaches a server bundle that still uses require:
Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.ts-node, running a .ts file compiled to CommonJS that requires an ESM package:
TSError: ⨯ Unable to compile TypeScript:
Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported.Why this happens
CommonJS require() is synchronous: it reads the file, evaluates it, and returns module.exports in one step, right where you called it. ES modules were designed to support things that cannot resolve synchronously, top-level await, live bindings between modules, and a resolution algorithm that can involve network-like fetches in browsers. For years, Node’s require() simply refused to load a .mjs file or any file a package.json marked with "type": "module", because it had no synchronous way to do it. That refusal is ERR_REQUIRE_ESM.
Meanwhile, hundreds of popular npm packages, chalk, node-fetch, execa, got, nanoid, ora, dropped CommonJS entirely in a major version and now ship only ESM. None of that is a bug in your code. It is a packaging decision made upstream, and require() hitting it is expected behavior, not a Node defect.
Check your Node version first
This is the step most other guides skip, and it is the biggest recent change to this error. Node added the ability for require() to load an ES module synchronously, called require(esm), and it is no longer experimental:
| Node.js version | require(esm) status |
|---|---|
| < 20.19.0 (on the 20.x line) | Not available, ERR_REQUIRE_ESM always throws |
| 20.19.0+ | Enabled by default, no flag needed |
| < 22.12.0 (on the 22.x line) | Behind --experimental-require-module |
| 22.12.0+ | Enabled by default, no flag needed |
| 23.0.0+ | Enabled by default from the start of the 23.x line |
Check what you are actually running:
node -vIf you are below the cutoff for your line, the fastest fix has nothing to do with your code:
nvm install --lts
nvm use --ltsIf you cannot upgrade Node yet but are close to the cutoff, the flag still works on the versions where it was experimental:
node --experimental-require-module app.jsOn a version that already has it stable, you can also explicitly turn it off with --no-experimental-require-module, which is worth knowing because some CI images or Docker base layers pin flags in NODE_OPTIONS that silently disable it.
The limitation that survives the upgrade: top-level await
Even on Node 22.12+, 20.19+, or 23+, require() still cannot load an ES module that uses top-level await, directly or in anything it imports. require() has to return synchronously, and a module suspended on an unresolved await cannot produce a value yet. Node gives this its own distinct error code, ERR_REQUIRE_ASYNC_MODULE, rather than falling back to ERR_REQUIRE_ESM:
Error [ERR_REQUIRE_ASYNC_MODULE]: require() of an ES Module ... that uses top-level await is not supported.This is the case that catches people who already upgraded and are confused why the error persists. There is no flag that fixes it, but there is one that helps you find it: --experimental-print-required-tla prints which module in the dependency tree contains the top-level await that is blocking the require() call, since it is often several layers deep in a transitive dependency rather than in the package you actually required. The fix is dynamic import(), covered below.
The other look-alike: ERR_PACKAGE_PATH_NOT_EXPORTED
Not every “can’t require this ESM package” failure is ERR_REQUIRE_ESM. Some packages deliberately block CommonJS entirely through their package.json exports map, by only declaring an "import" condition and no "require" or "default" fallback:
{
"exports": {
".": {
"import": "./index.mjs"
}
}
}With that shape, Node’s CommonJS resolver never finds a matching path at all, so it fails before the require(esm) machinery even gets involved:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /project/node_modules/pkg/package.jsonUpgrading Node does not fix this one, ever, on any version. The package author has intentionally opted out of CommonJS at the packaging level, not just shipped ESM syntax. The fixes are the same as the top-level-await case: dynamic import(), converting your project to ESM, or pinning an older version of the package that still exposes a require condition.
Use dynamic import() instead of require()
This works everywhere, on every Node version, regardless of which of the above you are hitting, because dynamic import() is available inside CommonJS modules:
// WRONG: synchronous require on an ESM-only package
const chalk = require('chalk');
console.log(chalk.green('Done'));// CORRECT: dynamic import, awaited inside an async function
async function main() {
const { default: chalk } = await import('chalk');
console.log(chalk.green('Done'));
}
main();At the top level of a CommonJS file, wrap it in an async IIFE since CommonJS files cannot use top-level await themselves:
(async () => {
const { default: chalk } = await import('chalk');
console.log(chalk.green('Done'));
})();If you need it once and want to cache it, a small lazy-loader avoids re-importing on every call:
let chalkPromise;
function getChalk() {
if (!chalkPromise) chalkPromise = import('chalk').then(m => m.default);
return chalkPromise;
}
async function log(msg) {
const chalk = await getChalk();
console.log(chalk.green(msg));
}The tradeoff is real: your function that used to be synchronous is now async, and that changes its call sites. For a one-off script this is trivial. For a library whose public API is synchronous, it can be a breaking change for your own users, which is exactly why the next fix is often the better long-term answer.
Convert your project to ESM
If most of your codebase is already using import/export syntax through a transpiler, finishing the conversion removes the CJS/ESM boundary entirely. Add the type field:
{
"type": "module"
}Then require() calls become import statements:
// Before
const chalk = require('chalk');
module.exports = { run };
// After
import chalk from 'chalk';
export { run };Two Node-specific gotchas when you flip the switch:
__dirnameand__filenamedo not exist in ESM. Reconstruct them fromimport.meta.url:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);- Relative imports need file extensions.
import './utils'fails; it must beimport './utils.js', even when importing a.tssource file compiled to.js(TypeScript’smoduleResolution: "NodeNext"enforces the same rule at the type-check level).
If only part of your package needs to stay CommonJS (a build tool config file, for instance), use the dual-extension escape hatch instead of converting everything: name files .mjs for ESM and .cjs for CommonJS, and Node respects the extension regardless of the "type" field in package.json.
Downgrade to the last CommonJS-compatible version
When you cannot touch the rest of the codebase right now, pin the dependency to the version before it dropped CJS support. These are the common ones people hit, with the last version known to still ship CommonJS:
| Package | Last CJS-friendly version | ESM-only from |
|---|---|---|
chalk | 4.x | 5.0.0 |
node-fetch | 2.x | 3.0.0 |
execa | 5.x | 6.0.0 |
got | 11.x | 12.0.0 |
nanoid | 3.x | 4.0.0 |
npm install chalk@4This is a stopgap, not a fix. I’ve spent more late nights than I’d like re-pinning chalk at v4 to unblock a release, and every single time an unrelated dependency eventually pulled in the ESM-only version anyway through its own tree, forcing the same decision later with less runway. Treat a pin as bought time to plan the ESM conversion, not a permanent answer.
Tool-specific fixes
ts-node. Add "module": "NodeNext" (or ESNext) to tsconfig.json and run with the ESM loader:
node --loader ts-node/esm script.tsOr switch to tsx, which handles ESM/CJS interop without any of the above configuration:
npm install -D tsx
npx tsx script.tsJest. Jest’s default transform assumes CommonJS. See Fix: Jest ESM Error for the full transform/extensionsToTreatAsEsm/moduleNameMapper setup needed to run tests against ESM-only dependencies.
Next.js. When an ESM-only package reaches server-side code through a require()-based bundling path, list it under transpilePackages in next.config.js so Next.js compiles it instead of requiring it raw:
// next.config.js
module.exports = {
transpilePackages: ['esm-only-package'],
};Webpack. Webpack bundles ESM packages statically at build time, so it does not go through Node’s require() loader and rarely triggers this error directly. If you see it anyway, it is usually a Node-side build script (a webpack config file, a plugin, a loader) that itself calls require() on an ESM-only package outside of the bundling process. Treat that config file the same as any other CommonJS file hitting this error: convert it to .mjs, or use dynamic import() inside it.
ESLint flat config. eslint.config.js follows the same rule as any other .js file: Node treats it as CommonJS or ESM based on your package.json "type" field, not based on anything ESLint-specific. If your project has no "type": "module" but you want to write the config with import/export syntax anyway, ESLint 9+ auto-discovers eslint.config.mjs, which is always ESM regardless of package.json.
Still not working?
Multiple copies of the same dependency. A monorepo or a deep transitive tree can end up with two installed copies of the same package, an old CJS one that a sibling package still requires directly and a new ESM one that another entry point resolves. Check with:
npm ls chalkIf you see more than one resolved version, the fix is deduping (npm dedupe) or aligning the version range across every package.json in the workspace so only one copy installs.
The error only appears after a production build, not in dev. Some bundlers apply different module resolution between dev server and production output (differences in fullySpecified, tree-shaking that changes which conditional export branch gets bundled). Build locally with the same command CI uses and reproduce it before assuming the fix worked.
A .cjs or .mjs extension is overriding what you expect. The file extension always wins over the package.json "type" field. A .cjs file is CommonJS even inside an ESM package, and a .mjs file is ESM even inside a CommonJS package. If a file behaves like the wrong module system, check its extension before checking package.json.
You changed "type": "module" and now plain .js config files break. Tools that still ship CommonJS config files (some ESLint, PostCSS, or Babel configs) will break the moment your package.json says "type": "module", because those .js files are now parsed as ESM too. Rename them to .cjs, or convert their contents to ESM syntax.
For related module-resolution errors, see Fix: Cannot use import statement outside a module, Fix: Node.js ERR_MODULE_NOT_FOUND, Fix: Jest ESM Error, and Fix: TypeScript Cannot Find Module.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: listen EADDRINUSE: address already in use (Node.js)
Fix Node.js 'listen EADDRINUSE: address already in use': find and kill the process on the port (macOS, Linux, Windows), or run your server on a different port.
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: Bun Not Working — Node.js Module Incompatible, Native Addon Fails, or bun test Errors
How to fix Bun runtime issues — Node.js API compatibility, native addons (node-gyp), Bun.serve vs Node http, bun test differences from Jest, and common package incompatibilities.
Fix: Node.js Stream Error — Pipe Not Working, Backpressure, or Premature Close
How to fix Node.js stream issues — pipe and pipeline errors, backpressure handling, Transform streams, async iteration, error propagation, and common stream anti-patterns.