Node.jsのERR_REQUIRE_ESMを見たら、最初に実際のNode.jsのバージョン、requireしたファイル、読み込まれるpackageとバージョンを確認します。「ESMはrequireでは一切読めない」という古い説明だけでは、現行Nodeの動作を判断できません。ソースがimportでも、生成コードやtest runnerがrequireへ変換している場合があります。
以下は診断用の架空例です。実packageのinstall、Node.jsのバージョン切替、moduleの読込実験、build・test runnerの実行は行っていません。利用版の公式資料と、実行時のエラーを照合して手順を選びます。
目次
エラーを出した実行環境を固定する
terminal、IDE、CI、test runnerが同じNodeを使うとは限りません。実行バージョンとpath、entrypoint、起動command、package manager、依存の解決済みバージョンを記録します。lockfileを消して再installする前に、直前の依存更新とruntime変更を確認します。
表は横にスクロールできます
| 記録 | 分ける対象 | 誤った判断 |
|---|---|---|
| Node.jsのバージョン | ローカルとCIの実行版 | 手元で通ったので全環境も通る |
| 呼出元ファイル | sourceか生成済みJSか | importを書いたのでrequireは存在しない |
| 読込先package | 解決済みバージョン・exports | package名だけでCJS/ESMを決める |
| エラーcode | REQUIRE_ESM・ASYNC_MODULE等 | すべて同じ置換で直す |
stack traceで最初の呼出境界を探します。自分のコードが直接requireしているのか、CLI設定や依存packageが読んでいるのかで、変更する場所が違います。node_modulesを直接編集して通す方法を、再現可能な修正にはしません。
現行Nodeのrequire対応を確認する
Node.jsのErrors資料(新しいタブで開く)は、同期ES moduleのrequire対応に伴いERR_REQUIRE_ESMをdeprecatedとしています。履歴にはNode 20.19.0、22.12.0、23.0.0で同期ESM読込が既定になったことが記載されています。これは当該版の保守状態や、利用packageすべての互換性を保証するものではありません。
現在のCommonJS modules資料(新しいタブで開く)では、requireで読めるESMには同期的であることなどの条件があります。依存のmodule graphにtop-level awaitがある場合は、ERR_REQUIRE_ASYNC_MODULEなど別の条件を調べます。新しいNodeへ上げれば、非同期のmoduleまで同期requireできると考えないようにします。
表は横にスクロールできます
| 状態 | 次の確認 | 選択肢の方向 |
|---|---|---|
| 古いruntimeでESM依存をrequire | packageの対応版とNode要件 | 対応runtimeまたは読込方式 |
| 現行runtimeで非同期ESMをrequire | top-level awaitを含む依存関係 | dynamic importなど非同期境界 |
| sourceはimport、生成物はrequire | transpiler・test runner出力 | 生成設定とruntimeの整合 |
| loaderや設定ファイルで失敗 | そのtoolのmodule対応 | 対象ファイルの形式を限定して変更 |
package typeと拡張子の範囲を読む
Packages資料(新しいタブで開く)で、.mjs、.cjs、.jsと近いpackage.jsonのtypeの関係を確認します。project rootのtypeだけで、依存packageや入れ子のpackage境界まで同じ形式と判断しません。
typeをmoduleへ変えると、その範囲の既存JSや設定ファイルが影響を受けます。require、module.exports、__dirname等を使うコードやtoolの設定を点検せず、rootの一行だけを変える修復にはしません。特定の設定ファイルだけCommonJSとして残すなら、そのtoolが対応する.cjsなどの方法を確認します。
表は横にスクロールできます
| 調整する範囲 | 確認するもの | 避ける変更 |
|---|---|---|
| app全体をESMへ寄せる | 起動・test・config・exports | typeだけ変更して終了 |
| CJSからESM依存を読む | dynamic importと非同期呼出元 | Promiseを同期値として使う |
| tool設定だけ残す | tool対応の拡張子・loader | 全packageの形式を変更 |
| 依存バージョンを調整 | 公式対応範囲と保守状況 | 理由なく古い版へ固定 |
dynamic importを使うなら呼出元まで確認する
CommonJSからdynamic importを使う方法は選択肢ですが、importの結果は非同期です。呼出元がawaitできるか、初期化順序、失敗時の処理を確認します。同期APIの中へそのまま差し込める保証はありません。
また、module namespaceとdefault exportは同じではありません。目的の関数をどこから取得するかは、対象packageのexportsと利用APIに合わせます。すべてのmoduleへ機械的に.defaultを足す処理や、関数名を推測する例にはしません。
依存バージョンを戻す場合は、戻す理由、対応するAPI、保守・修正の状態、lock差分を確認します。動いた古い版を永久に安全な版と扱わず、暫定措置なら解除条件を残します。
sourceと実行される生成物を同じ条件で確かめる
TypeScriptなどを使う場合は、sourceのimport文だけでなく、実行対象のJSがどのmodule形式で出力されたかを確認します。dev サーバーでは変換されても、Nodeで直接起動する成果物は別の経路です。test runnerだけで失敗する場合も、その変換設定とNode版を分けます。
修正候補は一つずつにし、最小の読込、実際のentrypoint、必要なtestを対象環境で確認します。エラーcodeが変わっただけなら、新しい失敗段階を調べます。moduleが読めてもAPI呼出の戻り値や副作用まで期待どおりとは限りません。
変更を戻せる記録を残す
package.json、lock、runtime指定、tool設定のどれを変えたかを記録し、元の対応関係を保ちます。失敗時に依存一式を消すのではなく、今回の最小差分を戻して原因を切り分けます。
完成記録には、元のNode/package版、失敗境界、採用したmodule方式、確認した起動・test経路、未確認の環境を残します。runtime更新だけ、拡張子変更だけで全体が修復したと断定しないことが重要です。
