Node.js で ESM の `ERR_MODULE_NOT_FOUND` を解く:解決策総当たりチェックリスト
Node.js で ESM を使っているときに突然現れる ERR_MODULE_NOT_FOUND エラー。インポート文は間違っていないはずなのに、なぜかモジュールが見つからないと怒られてしまう経験はありませんか?
このエラーは原因が多岐にわたるため、どこから手をつければいいのか迷ってしまいますよね。本記事では、このエラーの原因を体系的に整理し、すぐに試せる解決策をチェックリスト形式でご紹介します。
背景
Node.js は従来 CommonJS(require/module.exports)を採用していましたが、ES Modules(ESM)の標準化に伴い、import/export 構文のサポートが追加されました。
ESM を有効にするには package.json に "type": "module" を追加するか、ファイル拡張子を .mjs にする必要があります。しかし、この切り替えによって従来は動いていたモジュール解決のルールが変わるため、様々なエラーが発生しやすくなっています。
以下の図は、Node.js における ESM のモジュール解決フローを示したものです。
mermaidflowchart TD
start["import 文実行"] --> check1{"拡張子あり?"}
check1 -->|Yes| resolve1["指定パス通りに解決"]
check1 -->|No| check2{"package.json に<br/>type: module?"}
check2 -->|Yes| resolve2[".js .mjs .json を試行"]
check2 -->|No| resolve3["CommonJS 形式で解決"]
resolve1 --> found{"ファイル存在?"}
resolve2 --> found
resolve3 --> found
found -->|Yes| success["モジュール読込成功"]
found -->|No| error["ERR_MODULE_NOT_FOUND"]
ESM では拡張子の省略やディレクトリインデックスの自動解決が制限されるため、従来の CommonJS とは異なる挙動を理解する必要があります。
課題
ERR_MODULE_NOT_FOUND エラーが発生する原因は非常に多様です。以下のような問題が複合的に絡み合うことで、原因特定が困難になります。
エラーの典型パターン
typescriptError [ERR_MODULE_NOT_FOUND]: Cannot find module '/path/to/module'
このエラーメッセージは一見シンプルですが、実際には以下のような複数の原因が考えられます。
主な原因の分類
以下の図は、ERR_MODULE_NOT_FOUND が発生する主な原因を分類したものです。
mermaidflowchart LR
root["ERR_MODULE_NOT_FOUND<br/>発生原因"] --> cat1["パス解決の問題"]
root --> cat2["package.json<br/>設定の問題"]
root --> cat3["依存関係の問題"]
root --> cat4["ビルド・<br/>トランスパイルの問題"]
cat1 --> c1a["拡張子の省略"]
cat1 --> c1b["相対パスの誤り"]
cat1 --> c1c["index.js の自動解決"]
cat2 --> c2a["type フィールドの不一致"]
cat2 --> c2b["exports フィールドの設定ミス"]
cat3 --> c3a["パッケージ未インストール"]
cat3 --> c3b["バージョン不一致"]
cat4 --> c4a["TypeScript の outDir"]
cat4 --> c4b["トランスパイル後のパス"]
これらの原因を 1 つずつ確認していくことが、エラー解決の近道となります。
解決策
ここからは、ERR_MODULE_NOT_FOUND エラーを解決するためのチェックリストを順番にご紹介します。上から順に試していくことで、多くのケースで問題を特定できるでしょう。
チェック 1:拡張子を明示的に記載する
ESM では拡張子の省略ができません。CommonJS では許されていた以下のようなインポート文は、ESM ではエラーになります。
❌ 拡張子なし(エラーになる)
typescriptimport { myFunction } from './utils/helper';
✅ 拡張子あり(正しい)
typescriptimport { myFunction } from './utils/helper.js';
重要なポイント:TypeScript を使っている場合でも、インポート文では .js を指定する必要があります。
typescript// helper.ts ファイルをインポートする場合
import { myFunction } from './utils/helper.js'; // .ts ではなく .js
トランスパイル後は .js ファイルになるため、ソースコードでも .js 拡張子を使います。
チェック 2:index.js の自動解決を期待しない
CommonJS では、ディレクトリ名を指定すると自動的に index.js が読み込まれましたが、ESM ではこの機能は利用できません。
❌ ディレクトリ指定(エラーになる)
typescriptimport { Component } from './components';
✅ ファイル名まで明示(正しい)
typescriptimport { Component } from './components/index.js';
もしくは、個別のファイル名を指定します。
typescriptimport { Component } from './components/Component.js';
ディレクトリ構造によるモジュール整理を行いたい場合は、index.js を作成し、そこで再エクスポートするパターンが有効です。
チェック 3:package.json の type フィールドを確認
Node.js が .js ファイルを ESM として扱うには、package.json に以下の設定が必要です。
json{
"type": "module"
}
この設定がない場合、.js ファイルは CommonJS として扱われ、import 文を使うとエラーになります。
確認方法
プロジェクトルートの package.json を開き、type フィールドを確認してください。
| # | 設定値 | 挙動 |
|---|---|---|
| 1 | "type": "module" | .js を ESM として扱う |
| 2 | "type": "commonjs" | .js を CommonJS として扱う |
| 3 | フィールドなし | CommonJS として扱う(デフォルト) |
補足:特定のファイルだけを ESM にしたい場合は、拡張子を .mjs に変更する方法もあります。
チェック 4:相対パスと絶対パスを正しく使う
ESM では、相対パスは必ず ./ または ../ から始める必要があります。
❌ 相対パスの記号なし(エラーになる)
typescriptimport { helper } from 'utils/helper.js';
✅ ./ から始める(正しい)
typescriptimport { helper } from './utils/helper.js';
Node.js は ./ や ../ がないパスを「パッケージ名」として解釈し、node_modules から探そうとします。ローカルファイルをインポートする際は必ず相対パス記号を付けましょう。
チェック 5:node_modules のパッケージがインストールされているか確認
外部パッケージをインポートする場合、そのパッケージが正しくインストールされている必要があります。
エラー例
typescriptError [ERR_MODULE_NOT_FOUND]: Cannot find package 'express'
確認と解決方法
以下のコマンドでパッケージをインストールします。
bashyarn add express
すでにインストール済みの場合は、依存関係を再インストールしてみましょう。
bashyarn install
パッケージが package.json に記載されているかも確認してください。
チェック 6:package.json の exports フィールドを確認
最近のパッケージは package.json の exports フィールドでモジュールのエントリーポイントを制御していることがあります。
exports フィールドの例
json{
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils/index.js"
}
}
この設定がある場合、パッケージ内の特定のパスにアクセスするには、exports で公開されているパスを使う必要があります。
❌ 公開されていないパスへのアクセス
typescriptimport { helper } from 'my-package/dist/helper.js';
✅ exports で公開されたパスを使う
typescriptimport { helper } from 'my-package/utils';
自作パッケージを公開する際も、exports フィールドを正しく設定することで、利用者がエラーに遭遇しにくくなります。
チェック 7:TypeScript の設定を確認(tsconfig.json)
TypeScript を使っている場合、トランスパイル後のファイル配置がインポートパスと一致していない可能性があります。
tsconfig.json の重要な設定項目
json{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src"
}
}
| # | フィールド | 説明 |
|---|---|---|
| 1 | module | 出力形式(ESM なら ESNext または ES2020 など) |
| 2 | moduleResolution | モジュール解決方法(node または bundler) |
| 3 | outDir | トランスパイル後のファイル出力先 |
| 4 | rootDir | ソースコードのルートディレクトリ |
重要:outDir を設定している場合、実行時には dist ディレクトリ内のファイルを参照する必要があります。
bashnode dist/index.js
チェック 8:シンボリックリンクの問題
Yarn や npm のワークスペース機能を使っている場合、パッケージがシンボリックリンクとして配置されることがあります。Node.js の ESM ローダーはシンボリックリンクの実体パスを解決するため、意図しないパスでモジュール解決が行われることがあります。
確認方法
以下のコマンドでシンボリックリンクを確認できます。
bashls -la node_modules
解決方法
ワークスペース構成を見直すか、--preserve-symlinks オプションを使って Node.js を起動します。
bashnode --preserve-symlinks dist/index.js
このオプションにより、シンボリックリンクをそのまま扱うようになります。
チェック 9:キャッシュをクリアする
稀に、Node.js や Yarn のキャッシュが原因でエラーが発生することがあります。
Node.js のキャッシュクリア
Node.js 自体にはキャッシュクリア機能はありませんが、node_modules を削除して再インストールすることで解決する場合があります。
bashrm -rf node_modules
yarn install
Yarn のキャッシュクリア
bashyarn cache clean
これにより、Yarn が保持しているパッケージキャッシュがクリアされます。
チェック 10:Node.js のバージョンを確認
ESM のサポートは Node.js 12 以降ですが、安定した動作を期待するなら Node.js 14 以降の利用を推奨します。
バージョン確認
bashnode --version
バージョンアップ方法
nvm を使っている場合は、以下のコマンドで最新の LTS バージョンをインストールできます。
bashnvm install --lts
nvm use --lts
Node.js のバージョンが古いと、exports フィールドなど新しい機能が正しく動作しない可能性があります。
具体例
ここからは、実際によくあるエラーパターンとその解決方法を具体的に見ていきましょう。
ケース 1:拡張子なしのインポートでエラー
エラーが発生するコード
typescript// src/index.js
import { add } from './utils/math';
console.log(add(2, 3));
エラーメッセージ
typescriptError [ERR_MODULE_NOT_FOUND]: Cannot find module '/path/to/src/utils/math'
解決方法
拡張子を追加します。
typescript// src/index.js
import { add } from './utils/math.js';
console.log(add(2, 3));
ESM では拡張子の省略ができないため、必ず .js を明記する必要があります。
ケース 2:TypeScript で拡張子を .ts と書いてしまう
エラーが発生するコード
typescript// src/index.ts
import { User } from './models/User.ts'; // ❌ .ts は不可
const user = new User();
解決方法
TypeScript でも、インポート時は .js 拡張子 を使用します。
typescript// src/index.ts
import { User } from './models/User.js'; // ✅ .js を使う
const user = new User();
TypeScript はトランスパイル時に .ts を .js に変換するため、ソースコード上では .js を指定する必要があります。
ケース 3:index.js の自動解決を期待したケース
ディレクトリ構成
csssrc/
├── components/
│ ├── index.js
│ └── Button.js
└── index.js
エラーが発生するコード
typescript// src/index.js
import { Button } from './components'; // ❌ index.js は自動解決されない
解決方法
ファイル名を明示的に指定します。
typescript// src/index.js
import { Button } from './components/index.js'; // ✅ index.js まで書く
または、直接コンポーネントファイルを指定します。
typescriptimport { Button } from './components/Button.js';
以下の図は、ESM におけるディレクトリインポートの挙動を示したものです。
mermaidflowchart TD
import_dir["import from './components'"] --> check{"Node.js が<br/>index.js を<br/>自動解決?"}
check -->|CommonJS| ok["✅ 自動的に<br/>index.js を読込"]
check -->|ESM| ng["❌ ERR_MODULE_NOT_FOUND"]
import_file["import from './components/index.js'"] --> resolve["✅ 明示的に<br/>ファイル指定"]
ケース 4:package.json に type: module が抜けている
package.json
json{
"name": "my-app",
"version": "1.0.0"
}
エラーが発生するコード
typescript// index.js
import express from 'express'; // ❌ type: module がないと SyntaxError
エラーメッセージ
typescriptSyntaxError: Cannot use import statement outside a module
解決方法
package.json に type フィールドを追加します。
json{
"name": "my-app",
"version": "1.0.0",
"type": "module"
}
これで .js ファイルが ESM として認識されます。
ケース 5:外部パッケージの exports 制約
あるパッケージの package.json
json{
"exports": {
".": "./dist/index.js"
}
}
エラーが発生するコード
typescriptimport { helper } from 'some-package/dist/utils.js'; // ❌ 公開されていない
エラーメッセージ
typescriptError [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/utils.js' is not defined by "exports"
解決方法
exports で公開されているパスのみを使用します。
typescriptimport pkg from 'some-package'; // ✅ メインエントリーポイントを使う
または、パッケージ作成者に exports フィールドの追加をリクエストします。
まとめ
本記事では、Node.js の ESM で発生する ERR_MODULE_NOT_FOUND エラーの原因と解決策をチェックリスト形式でご紹介しました。
図で理解できる要点:
- ESM ではモジュール解決ルールが CommonJS と異なる
- 拡張子省略や
index.js自動解決は利用できない package.jsonの設定とパス指定の正確さが重要
解決のための主なチェックポイント:
| # | チェック項目 | 確認内容 |
|---|---|---|
| 1 | 拡張子の明示 | .js 拡張子を省略していないか |
| 2 | index.js の期待 | ディレクトリ名だけでインポートしていないか |
| 3 | type フィールド | package.json に "type": "module" があるか |
| 4 | 相対パスの記号 | ./ または ../ から始まっているか |
| 5 | パッケージのインストール | 必要なパッケージが node_modules にあるか |
| 6 | exports フィールド | パッケージの公開パスを確認したか |
| 7 | TypeScript 設定 | tsconfig.json の outDir と実行パスが一致しているか |
| 8 | シンボリックリンク | ワークスペースでのリンク問題がないか |
| 9 | キャッシュ | node_modules と Yarn キャッシュをクリアしたか |
| 10 | Node.js バージョン | ESM サポートのあるバージョンか |
ESM への移行は一見複雑に思えますが、これらのポイントを押さえることで多くの問題を解決できます。エラーが出たときは、焦らずこのチェックリストを上から順に確認してみてください。
きっと原因が見つかるはずです。もし解決しない場合は、エラーメッセージの全文と環境情報を添えて、コミュニティやフォーラムに質問してみましょう。
関連リンク
articleNode.js で ESM の `ERR_MODULE_NOT_FOUND` を解く:解決策総当たりチェックリスト
articleNode.js 本番メモリ運用:ヒープ/外部メモリ/リーク検知の継続監視
articleNode.js で社内 RPA:Playwright でブラウザ自動化&失敗回復の流儀
articleNode.js × Fastify で爆速 REST API:スキーマ駆動とプラグイン設計を学ぶ
articleNode.js クリーンアーキテクチャ実践:アダプタ/ユースケース/エンティティの分離
articlehtmx × Express/Node.js 高速セットアップ:テンプレ・部分テンプレ構成の定石
articleNotebookLM 情報設計のベストプラクティス:ソース粒度・タグ・命名規則
articleRedis 監視と可観測性:Prometheus Exporter と Grafana の実践ダッシュボード
articleNode.js で ESM の `ERR_MODULE_NOT_FOUND` を解く:解決策総当たりチェックリスト
articleReact 開発環境の作り方:Vite + TypeScript + ESLint + Prettier 完全セットアップ
articlePython エコシステム地図 2025:データ・Web・ML・自動化の最短ルート
articleNext.js でドキュメントポータル:MDX/全文検索/バージョン切替の設計例
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来