T-CREATOR

Node.js で ESM の `ERR_MODULE_NOT_FOUND` を解く:解決策総当たりチェックリスト

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.jsontype フィールドを確認

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.jsonexports フィールドを確認

最近のパッケージは package.jsonexports フィールドでモジュールのエントリーポイントを制御していることがあります。

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"
  }
}
#フィールド説明
1module出力形式(ESM なら ESNext または ES2020 など)
2moduleResolutionモジュール解決方法(node または bundler
3outDirトランスパイル後のファイル出力先
4rootDirソースコードのルートディレクトリ

重要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.jsontype: 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.jsontype フィールドを追加します。

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 拡張子を省略していないか
2index.js の期待ディレクトリ名だけでインポートしていないか
3type フィールドpackage.json"type": "module" があるか
4相対パスの記号.​/​ または ..​/​ から始まっているか
5パッケージのインストール必要なパッケージが node_modules にあるか
6exports フィールドパッケージの公開パスを確認したか
7TypeScript 設定tsconfig.jsonoutDir と実行パスが一致しているか
8シンボリックリンクワークスペースでのリンク問題がないか
9キャッシュnode_modules と Yarn キャッシュをクリアしたか
10Node.js バージョンESM サポートのあるバージョンか

ESM への移行は一見複雑に思えますが、これらのポイントを押さえることで多くの問題を解決できます。エラーが出たときは、焦らずこのチェックリストを上から順に確認してみてください。

きっと原因が見つかるはずです。もし解決しない場合は、エラーメッセージの全文と環境情報を添えて、コミュニティやフォーラムに質問してみましょう。

関連リンク