← 記事一覧に戻る

Node.js で ERR_REQUIRE_ESM エラーが出る・require() が動かない時の原因と5つの対策【ESM移行ガイド】

ある日、npmパッケージをアップデートしたら突然こんなエラーが出た——という経験はないだろうか。

Error [ERR_REQUIRE_ESM]: require() of ES Module
/path/to/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.

chalk v5・node-fetch v3・p-limit v4・globby v13 など、多くの人気パッケージが ESM(ES Modules)専用になったことで、CommonJS(CJS)プロジェクトでそのまま使えなくなった。この記事では根本原因と、状況に応じた5つの対処法を解説する。


ERR_REQUIRE_ESM が出るパッケージ一覧(主要なもの)

パッケージ ESM専用になったバージョン
chalk v5.0.0 以降
node-fetch v3.0.0 以降
p-limit v4.0.0 以降
globby v13.0.0 以降
execa v6.0.0 以降
got v12.0.0 以降
ora v6.0.0 以降
nanoid v4.0.0 以降
find-up v6.0.0 以降
is-stream v3.0.0 以降

package.json"type": "module" が記載されているか、exports フィールドで import 条件のみ提供されているパッケージは ESM 専用だ。


なぜ ERR_REQUIRE_ESM が起きるのか

Node.js のモジュールシステムには2種類ある。

CommonJS(CJS)

  • require() / module.exports を使う旧来の形式
  • package.json"type" 指定がない場合、.js はデフォルトで CJS

ES Modules(ESM)

  • import / export を使うモダンな形式
  • package.json"type": "module" が必要、または .mjs 拡張子を使う

問題の核心require() は同期的に動作するため、非同期で解決される ESM モジュールを読み込めない。これは Node.js の仕様上の制約であり、設定で回避できるものではない。


対処法1:パッケージをバージョンダウンする(応急処置)

ESM 専用になる前のバージョンに固定する。最も手軽だが一時しのぎにすぎない。

# chalk の場合(v4 は CJS 対応)
npm install chalk@4

# node-fetch の場合(v2 は CJS 対応)
npm install node-fetch@2

# p-limit の場合(v3 は CJS 対応)
npm install p-limit@3
// chalk v4 は CJS で問題なく使える
const chalk = require('chalk')

console.log(chalk.blue('Hello, world!'))

デメリット:セキュリティパッチ・新機能が受け取れない。根本解決にならない。


対処法2:dynamic import() を使う(推奨)

require() の代わりに import() を使う。import()CJS ファイルからでも呼べるため、ファイル形式を変えずに ESM パッケージを利用できる。

// Before(エラーになる)
const chalk = require('chalk')

// After(動く)
const { default: chalk } = await import('chalk')
console.log(chalk.blue('Hello!'))

await が必要なため、非同期関数の中で使う必要がある。トップレベルの await は ESM ファイルでのみ使用可能だ。

// CJS ファイル内での実用例
async function main() {
  const { default: chalk } = await import('chalk')
  const { default: fetch } = await import('node-fetch')

  console.log(chalk.green('Starting...'))

  const res = await fetch('https://api.example.com/data')
  const data = await res.json()
  console.log(data)
}

main().catch(console.error)

デメリット:関数内でしか使えず、モジュールスコープでの定数として扱いにくい。


対処法3:ファイルを .mjs に変更する(部分的 ESM 化)

ESM パッケージを使いたいファイルだけ .mjs 拡張子に変更する。Node.js は .mjs を常に ESM として扱う。

mv src/utils/mailer.js src/utils/mailer.mjs
// mailer.mjs — ESM 形式で書ける
import chalk from 'chalk'
import fetch from 'node-fetch'

export async function sendMail(to, subject, body) {
  console.log(chalk.green(`Sending mail to ${to}...`))
  // ...
}

CJS ファイルから .mjs を呼ぶ場合は import() が必要になる:

// main.js(CJS)から .mjs ファイルを使う
async function run() {
  const { sendMail } = await import('./utils/mailer.mjs')
  await sendMail('user@example.com', 'Hello', 'Body text')
}

run()

デメリット:CJS と ESM が混在してコードベースが複雑になりやすい。


対処法4:プロジェクト全体を ESM 化する(本命)

package.json"type": "module" を追加し、プロジェクト全体を ESM に移行する。新規プロジェクトや比較的小さなコードベースに向いている。

{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module"
}

全ての .js ファイルを ESM 形式に書き換える:

// Before(CJS)
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')

function greet(name) {
  console.log(chalk.blue(`Hello, ${name}!`))
}

module.exports = { greet }

// After(ESM)
import fs from 'fs'
import path from 'path'
import chalk from 'chalk'

export function greet(name) {
  console.log(chalk.blue(`Hello, ${name}!`))
}

ESM 移行時の注意:__dirname と __filename が使えない

ESM では __dirname__filename がデフォルトで使えない。次の代替コードを使う:

// ESM での __dirname / __filename の代替
import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

// Node.js 21.2.0 以降はこれだけでOK
// const __dirname = import.meta.dirname

ESM 移行時の注意:Jest が動かなくなる

ESM 化後に Jest を使う場合、追加設定または実験的フラグが必要になる:

# Jest を ESM モードで実行
node --experimental-vm-modules node_modules/.bin/jest

これが面倒な場合は Vitest への移行を検討しよう。Vitest は Vite ベースで ESM をネイティブサポートしており、Jest と API 互換性が高い。


対処法5:TypeScript + ts-node を使っている場合

TypeScript プロジェクトで ts-node を使っている場合、ESM 化には追加設定が必要だ。

// tsconfig.json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}
// package.json
{
  "type": "module",
  "scripts": {
    "dev": "node --loader ts-node/esm src/index.ts"
  }
}

ただし、設定が煩雑になりがちだ。tsx を使うほうがシンプルでトラブルも少ない:

npm install -D tsx
{
  "scripts": {
    "dev": "tsx src/index.ts",
    "start": "tsx src/index.ts"
  }
}

tsx は CJS・ESM を自動判別するため、package.json"type" 設定にかかわらず動作する。設定を最小限に抑えたいプロジェクトに最適だ。


どの対処法を選ぶべきか

状況ごとの判断基準をまとめる。

状況 推奨対処法
新規プロジェクト 対処法4(最初から ESM で始める)
小〜中規模の既存プロジェクト 対処法4(ESM 化に踏み切る)
大規模・レガシーな既存プロジェクト 対処法2(dynamic import で段階的に対応)
急いでいる・一時的な対応 対処法1(バージョンダウン)
TypeScript + ts-node 対処法5(tsx への移行を検討)
特定ファイルだけ ESM パッケージを使いたい 対処法3(.mjs ファイル化)

よくあるつまずきポイント

“type”: “module” にしたら既存コードが一斉に壊れた

ESM 化後は require() が一切使えなくなる。全ての require()import に書き換える必要がある。一気に移行が難しい場合は、対処法2(dynamic import)で段階的に対応するか、ファイル単位で .mjs / .cjs 拡張子を使って混在させる方法もある。

CJS のパッケージを ESM から使う場合は問題ない

逆パターン(ESM プロジェクトから CJS パッケージを import)は Node.js が自動変換するため問題なく動く。エラーが起きるのは CJS から ESM を require() する方向だけだ。

require.resolve() も同様にエラーになる

require() だけでなく require.resolve() も ESM パッケージには使えない。パスの解決には import.meta.resolve()(Node.js 20.6.0 以降)を使う:

// ESM でのパス解決
const resolvedPath = import.meta.resolve('chalk')
console.log(resolvedPath) // file:///path/to/node_modules/chalk/source/index.js

まとめ

ERR_REQUIRE_ESM は Node.js のモジュールシステム移行期に避けられないエラーだ。根本的な解決はプロジェクトの ESM 化だが、コードベースの規模や緊急度に応じて対処法を選ぶのが現実的だ。

原因 対策
chalk v5+ など人気パッケージの ESM 専用化 バージョンダウン or dynamic import
require() は同期的で ESM を読めない import() は非同期で CJS からも使える
プロジェクト全体の移行が必要 "type": "module" + 全ファイル書き換え
TypeScript + ts-node 環境 tsx への移行が最もシンプル

長期的には ESM への完全移行が正解だ。新規プロジェクトは最初から ESM で始め、既存プロジェクトは dynamic import を足がかりに段階的に移行していくのがよいだろう。