What this usually means
This error occurs when your codebase mixes ES module (.mjs, 'type':'module') and CommonJS (.cjs, default) syntax. Node.js strictly enforces that require() can only load CommonJS modules, not ES modules. The root cause is usually a package.json missing or conflicting 'type' field, importing a .mjs file with require(), or a dependency that ships ES-only exports. The error isn't random—it's Node.js enforcing module resolution rules.
The first ten minutes — establish facts before touching code.
- 1Check the error stack trace: find the exact file doing require() and the file being required.
- 2Run 'node --input-type=module -e "console.log(1)" && echo OK' to confirm Node.js version supports ESM.
- 3Inspect the 'type' field in all package.json files along the require() chain.
- 4Use 'node -e "console.log(require.resolve('./your-file.js'))"' to see what Node resolves.
- 5Check the file extension: .mjs forces ESM, .cjs forces CommonJS, .js depends on package.json 'type'.
- 6Run 'node --experimental-modules' (deprecated) if on Node <13; otherwise upgrade.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchpackage.json files (root and any local node_modules)
- searchThe exact require() call in the stack trace
- searchFile extensions of the imported module: .mjs, .cjs, .js
- searchNode.js version: 'node --version'
- searchDependency source: check node_modules/<module>/package.json for 'type' and 'exports'
- searchtsconfig.json if using TypeScript with 'module' setting
- searchWebpack/Rollup config files if using a bundler
Practical causes, not theory. These are the things you will actually find.
- warningMissing 'type' field in package.json (defaults to CommonJS, but dependency may be ESM)
- warningSetting 'type': 'module' in root package.json but using require() in .js files
- warningThird-party library ships only as ESM (e.g., node-fetch v3, chalk v5)
- warningRenaming .js to .mjs but forgetting to update require() calls
- warningUsing 'exports' map in package.json with 'import' but not 'require' condition
- warningTypeScript compiled to ES modules but runtime Node expects CommonJS
- warningDocker image has different Node version than local environment
Concrete fix directions. Pick the one that matches your root cause.
- buildReplace require() with dynamic import(): 'const mod = await import('module')'
- buildRename .js file to .cjs to force CommonJS, or add 'type': 'commonjs' in a sub-package.json
- buildSet 'type': 'module' in package.json and convert all require() to import/export syntax
- buildUse a bundler like Webpack/Rollup to handle module resolution
- buildUpdate third-party dependency to a version that provides CommonJS exports
- buildUse .mjs extension for ES modules and .cjs for CommonJS to avoid ambiguity
- buildIn package.json 'exports', add 'require' condition that points to a CommonJS file
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the application after fix: should not throw ERR_REQUIRE_ESM
- verifiedUse 'node --check' to validate syntax changes
- verifiedTest both import and require scenarios if mixed module usage is intentional
- verifiedRun unit tests that cover the failing import path
- verifiedCheck that the fix works in CI/Docker with same Node version
- verifiedValidate that third-party module version supports your module system
Things that make this bug worse or harder to find.
- warningDo not blindly add 'type':'module' without converting all require() to import
- warningDo not use 'await import()' in non-async functions without proper handling
- warningDo not ignore the stack trace—it pinpoints the exact require() call
- warningDo not use --experimental-modules flag on Node >=14 (it's deprecated)
- warningDo not assume all dependencies have CommonJS fallback; check their package.json
- warningDo not mix .js with different 'type' in same directory without explicit extensions
node-fetch v3 upgrade breaks production API gateway
Timeline
- 09:15Deploy new node-fetch v3 to production API gateway
- 09:18PagerDuty alert: 'ERR_REQUIRE_ESM' in /api/users endpoint
- 09:20Rollback to node-fetch v2 to mitigate
- 09:30Check package.json: root has 'type': 'module'? No, it's CommonJS
- 09:45Inspect node_modules/node-fetch/package.json: 'type': 'module', no 'exports' CommonJS fallback
- 10:00Attempt to use dynamic import: const fetch = (await import('node-fetch')).default
- 10:05Deploy fix: change require('node-fetch') to dynamic import in the one file that uses it
- 10:08Monitor: no errors, latency normal
We had a simple API gateway using Express. It used node-fetch to forward requests to a microservice. The code was all CommonJS with require(). We upgraded node-fetch from v2 to v3 for new features. Deployment went fine, but within minutes the /api/users endpoint started throwing ERR_REQUIRE_ESM. The stack trace showed the require('node-fetch') line. I rolled back immediately.
I checked node_modules/node-fetch/package.json: node-fetch v3 is ESM-only. It has 'type': 'module' and its exports field only has 'import', no 'require' condition. Our codebase is CommonJS—we never set 'type' in package.json, so .js files are treated as CommonJS. require() cannot load an ES module.
The fix was straightforward: in the single file that required node-fetch, I replaced const fetch = require('node-fetch') with const fetch = (await import('node-fetch')).default. I also had to make the containing function async. I deployed and monitored—no errors. The lesson: always check a dependency's package.json for module system before upgrading, especially if it's a major version bump.
Root cause
node-fetch v3 is ESM-only, but the application used require() in CommonJS context.
The fix
Replace require('node-fetch') with dynamic import and use .default.
The lesson
Check third-party module's package.json for 'type' and 'exports' before upgrading major versions.
Node.js uses file extension and nearest package.json 'type' field to decide if a file is CommonJS or ES module. The priority: .mjs forces ESM, .cjs forces CommonJS, .js uses the 'type' field of the nearest package.json (defaults to 'commonjs' if absent).
This means two .js files in different directories can be treated differently if their package.json 'type' differs. The error 'require() of ES Module' occurs when a CommonJS file uses require() on a file Node.js classifies as ESM. Understanding this resolution is critical.
Modern packages use the 'exports' field in package.json to provide separate entry points for import and require. A common pattern: 'exports': { 'import': './esm/index.js', 'require': './cjs/index.cjs' }. If a package provides only an 'import' condition, require() will fail.
Packages like node-fetch v3 and chalk v5 ship ESM-only. They expect consumers to use import. If you must use require(), consider a bundler or downgrade to a CommonJS version. Alternatively, you can create a wrapper CommonJS module that re-exports via dynamic import.
Dynamic import() returns a Promise of the module namespace. For default exports, you need to access .default. Example: const mod = await import('./esm-module.js'); const defaultExport = mod.default;. This works in async functions or top-level await (with 'type':'module' or .mjs).
Common mistakes: forgetting to await import(), using import() in a synchronous context without handling the Promise, and assuming the module exports a default when it might export named exports. Also, dynamic imports can slow down startup—consider caching the result.
The simplest way to avoid confusion is to use explicit extensions: .mjs for ES modules, .cjs for CommonJS. This overrides any package.json 'type' setting. If a dependency is ESM-only, you can create a .cjs wrapper that uses dynamic import and re-exports via module.exports.
For codebases transitioning from CommonJS to ESM, gradually rename files to .mjs and update imports. This avoids the 'type' field cascade and makes module system explicit. Tools like 'esm' (deprecated) are not needed on Node >=14.
Frequently asked questions
Can I use require() to load a .mjs file?
No, require() cannot load ES modules (including .mjs files). You must use import() or convert the file to CommonJS (.cjs or remove 'type':'module'). Node.js enforces this strictly.
Why did my package work before Node 14 and now fails?
Node 12 introduced experimental ESM support behind a flag; Node 14 made it stable. If you were using --experimental-modules before, that flag is now deprecated and the behavior changed. Also, many packages dropped CommonJS support in recent major versions.
How do I check if a third-party package supports CommonJS?
Look at the 'exports' field in the package's package.json. If it has a 'require' condition or the package has a 'main' field pointing to a .js file (without 'type':'module'), it likely supports CommonJS. If only 'import' is present, it's ESM-only.
Does TypeScript compile to ESM or CommonJS?
It depends on your tsconfig.json 'module' setting. If set to 'commonjs', output uses require(). If set to 'esnext' or 'es2020', output uses import/export. Ensure your Node.js runtime matches the compiled module system, or handle the mismatch with appropriate extensions.
What's the best way to transition a large codebase from CommonJS to ESM?
Start by adding 'type':'module' in package.json and rename all .js files to .mjs temporarily to identify issues. Update require() calls to import statements gradually. Use a linter to catch mixed usage. Test thoroughly with Node --experimental-vm-modules if needed. Alternatively, use a bundler to output CommonJS while you develop in ESM.