> ## Content Index
> Fetch the complete content index at: https://debugly.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# ERR_REQUIRE_ESM: The Complete Guide to Why Your Import Broke
- URL: https://debugly.dev/err-require-esm-node-fix/
- Published: 2026-03-20T08:55:00.000Z
- Updated: 2026-10-17T05:58:52.000Z
- Description: The error that shows up when a dependency goes ESM only. What it means, the five ways to fix it, and how to pick the right one.
- Author: Rohit Bhadani
- Tags: Node.js, JavaScript, Error Autopsy

## The quick version

```
Error [ERR_REQUIRE_ESM]: require() of ES Module /node_modules/chalk/source/index.js
from /app/index.js not supported.

```

A CommonJS file is trying to `require()` a package that ships only ES modules. Five options, in rough order of preference:

1. **Upgrade Node to 22.12 or later, or 20.19 or later.** `require(esm)` now works for most modules and this alone often fixes it.
2. **Convert your project to ESM** with `"type": "module"`. Correct long term.
3. **Use dynamic `await import()`.** Smallest change if only one call site is affected.
4. **Pin the dependency to its last CommonJS version.** Buys time, accrues debt.
5. **Bundle both formats** with tsup or esbuild. Right answer if you are publishing a library.

Tested on Node 22.14 and npm 10.9\. The `require(esm)` support described below landed in Node 22.12 and was backported to 20.19.

## Why this happens at all

JavaScript has two module systems with fundamentally different loading semantics.

CommonJS resolves and executes synchronously. When you call `require('./thing')`, Node reads the file, runs it top to bottom, and returns `module.exports`, all before your next line runs.

ES modules are asynchronous by design. The specification defines three phases: parse the whole graph to discover imports, link the bindings, then evaluate. Top level `await` means evaluation itself can suspend.

You cannot express "wait for an async operation" inside a synchronous function call. That is the entire reason requiring an ES module was an error for years. Not stubbornness, an actual impedance mismatch.

What changed is that Node 22.12 shipped `require(esm)`. When you require an ES module, Node now synchronously evaluates it, provided the graph contains no top level `await`. If it does, you get a different error, `ERR_REQUIRE_ASYNC_MODULE`, and you are back to needing a real fix. So the constraint did not disappear, it narrowed to the genuinely impossible case.

This is why upgrading Node is the first thing to try. A large share of packages that broke builds in 2023 simply work now.

## Confirm the diagnosis

Two questions. Is the dependency really ESM only, and is your file really CommonJS?

**Is the package ESM only?**

```bash
cat node_modules/chalk/package.json | jq '{type, main, module, exports}'

```

```json
{
  "type": "module",
  "main": null,
  "exports": "./source/index.js"
}

```

`"type": "module"` with no CommonJS entry in `exports` means ESM only. If instead you see an `exports` map containing a `require` condition, the package is dual and something else is wrong, usually a bundler resolving the wrong condition.

**Is your file CommonJS?** It is if your nearest package.json lacks `"type": "module"` and the extension is `.js`, or if the extension is `.cjs`.

TypeScript adds a wrinkle. If your tsconfig has `"module": "commonjs"`, your `import` statements compile down to `require()` calls. The source looks like ESM and the output is not. This is the single most common source of confusion with this error, because the fix has to happen in tsconfig.json rather than in the source file.

## The fixes

### 1\. Upgrade Node

```bash
node -v          # need 22.12.0 or later, or 20.19.0 or later

```

Try this before anything else. If the package has no top level `await` anywhere in its import graph, `require()` now works and you can stop reading.

If you get `ERR_REQUIRE_ASYNC_MODULE` after upgrading, the package uses top level `await` and you need one of the options below.

One interop detail to budget for: `require()` of an ES module returns the module namespace object, not the default export.

```js
const chalk = require('chalk');        // { default: [Function], Chalk: ... }
chalk('hi');                            // TypeError: chalk is not a function
chalk.default('hi');                    // works

```

Node adds a `module.exports` style shortcut when the ESM module has only a default export, but for mixed exports you need `.default`. This produces a confusing second error right after you fix the first one.

### 2\. Convert your project to ESM

The correct destination. In package.json:

```json
{ "type": "module" }

```

Then the migration work:

**Add file extensions to relative imports.** `import './utils'` becomes `import './utils.js'`. ESM does not do extension resolution. In TypeScript you write `.js` even though the file is `.ts`, which looks wrong and is correct.

**Replace `__dirname` and `__filename`:**

```js
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));

```

Or on Node 20.11 and later, just `import.meta.dirname`.

**Replace `require.resolve`** with `import.meta.resolve`.

**JSON imports need an attribute:** `import pkg from './package.json' with { type: 'json' };`

**Jest** needs `--experimental-vm-modules`, or move to Vitest which handles ESM natively.

Any remaining CommonJS files get renamed to `.cjs`.

For TypeScript the matching config is:

```json
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2022"
  }
}

```

`nodenext` is the setting that makes TypeScript model Node's actual dual module behaviour, including telling you when an import will not work at runtime. Anything else and TypeScript will happily compile code that throws.

### 3\. Dynamic import

If exactly one place uses the offending package, this is a two line change:

```js
// before
const chalk = require('chalk');

// after
const chalk = (await import('chalk')).default;

```

`import()` is asynchronous and therefore has no impedance mismatch. It works from CommonJS on every Node version.

The catch is that `await` needs an async context. At the top level of a CommonJS module you have to wrap it:

```js
let chalk;
async function init() {
  ({ default: chalk } = await import('chalk'));
}

```

Which introduces an initialisation ordering problem. Fine for a CLI with a clear entry point, awkward inside a library.

There is a TypeScript trap here worth flagging. With `"module": "commonjs"`, TypeScript compiles your `await import()` into `Promise.resolve().then(() => require())`, reintroducing exactly the `require()` you were trying to avoid. Set `"module": "node16"` or `"nodenext"`. This trips up a lot of people who apply the fix correctly and see no change at all.

### 4\. Pin the dependency

```bash
npm install chalk@4

```

Many popular packages have a final CommonJS release: chalk 4, node-fetch 2, nanoid 3, got 11, strip-ansi 6, execa 5, p-limit 3.

This works and I am not going to pretend otherwise. But you are now on a version that will not get security patches, and you will hit the same wall with the next package. Reasonable as a deliberate ninety day decision, bad as a permanent one. Leave a comment saying which one it is.

For transitive dependencies you do not control directly, npm overrides can force a version:

```json
{ "overrides": { "strip-ansi": "6.0.1" } }

```

### 5\. Bundle both formats

If you are publishing a library, build both. tsup does it in one line:

```bash
tsup src/index.ts --format cjs,esm --dts

```

```json
{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

```

Order matters inside the conditions object. `types` first, more specific conditions before `default`, because Node picks the first match.

## The dual package hazard

Once you ship both formats you can end up with two copies of the same module in one process, one loaded through `import` and one through `require`. Separate module registries, separate state.

Invisible until it is not:

```js
// instanceof fails across the boundary
err instanceof MyCustomError   // false, different class object

// singletons are not singletons
registry.register('x')          // registered on the CommonJS copy
registry.list()                 // called on the ESM copy, returns []

```

Mitigate by keeping all stateful logic in a CommonJS core and making the ESM entry a thin re export wrapper, so both formats share one instance. Or avoid module level state entirely, which is better design regardless.

## Finding which file is at fault

The error names the file doing the `require()`, but in a deep dependency tree that file is often not yours.

```bash
node --trace-warnings index.js
NODE_DEBUG=module node index.js      # very verbose
npm ls chalk                          # who depends on it

```

If the requiring file is inside `node_modules`, you cannot fix it directly. A dependency of yours is CommonJS and depends on an ESM only package. Your options are updating that intermediate dependency, or overriding the inner one to a CommonJS version.

## Prevention

**Start new projects with `"type": "module"`.** The ecosystem's direction is settled, and starting in CommonJS is choosing to migrate later.

**Use `"module": "nodenext"` in TypeScript.** It surfaces these problems at compile time instead of at runtime in production.

**Run CI on the Node version you actually deploy.** A `require(esm)` that works on Node 22 locally and fails on Node 18 in production is a bad afternoon.

**Check `exports` before adding a dependency.** `npm view <pkg> exports` takes two seconds.

---

Published on [debugly.dev](https://debugly.dev/) by [Rohit Bhadani](https://debugly.dev/about/) | Founder of [Krova Cloud](https://krova.cloud/?ref=debugly.dev)