Error [ERESOLVE unable to resolve dependency tree]

ERESOLVE unable to resolve dependency tree の直し方(npm install が peer dependency の衝突で止まる)

FIX SUMMARY verified
Applies when
node:22npm 7+ / react-dompeer dependency conflict on npm install

Verified: reproduced in node:22, then the ERESOLVE unable to resolve dependency tree signature was gone after the fix (exit 0).

npm install が、パッケージを1つも入れないまま次のエラーで止まることがあります。

npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree

npm のバージョンによっては、各行の先頭が npm error ではなく npm ERR! と表示されます(npm 9 系まで)。中身は同じです。

止まる直前に、どの依存とどの依存がぶつかっているかが続けて表示されます。たとえば次のような形です。

npm error While resolving: [email protected]
npm error Found: [email protected]
npm error node_modules/react
npm error   react@"18.3.1" from the root project
npm error
npm error Could not resolve dependency:
npm error peer react@"^19.0.0" from [email protected]
npm error node_modules/react-dom
npm error   react-dom@"19.0.0" from the root project

これは「あるパッケージが要求する peer dependency(同居してほしい依存のバージョン) が、いま入れようとしている構成と食い違っている」という報告を意味します。上の例なら、[email protected]react@^19.0.0 を求めているのに、プロジェクトが [email protected] を固定している、という衝突です。

このエラーで検索すると、多くのページが真っ先に --legacy-peer-deps を付けろと勧めます。これはたしかにエラーを消しますが、問題を install の時点から実行時へ先送りするだけのことがあります。その理由は、npm のバージョンによる挙動の違いにあるのです。

まず、何が衝突しているのかを読む

ERESOLVE のメッセージは長く見えますが、読む場所は決まっています。

  • Found: … いまツリーに入っている(あるいは入れようとしている)バージョン。上の例では [email protected]
  • Could not resolve dependency: の下の peer ... … 誰が、どのバージョンを要求しているか。上の例では [email protected]react@^19.0.0 を要求。

この2つを突き合わせると、「react-dom@19react@19 を求めているが、手元は react@18」という一行にまとまります。要求元(react-dom)と、要求されている側(react)のどちらのバージョンを動かせば揃うかが、そのまま直し方になります。

バージョン境界:同じ install が npm 6 では通り、npm 7 以降で止まる

このエラーは、あなたの package.json が変わったから急に出たとは限りません。npm 自身の挙動が変わったために、以前は黙って通っていた構成が止まるようになった、という場合があります。

npm のバージョン同梱される Node の例同じ衝突した構成の挙動
6 系Node 14警告だけ出して、そのまま入れる(終了コード 0)
8 系 / 10 系Node 16 / Node 22ERESOLVE で停止し、node_modules を作らない

境界は npm 7 です。npm 7 から、peer dependency が自動でインストールされ、ルート直下で自動解決できない衝突は既定でエラーになるようになりました(深くネストした衝突は既定では警告のみで、全階層を厳格にするには --strict-peer-deps が要ります)。この記事の react / react-dom は直下の衝突なので、既定で止まります。npm 6 は同じ構成に対して次の警告を出すだけで、依存をそのまま展開していました。

npm WARN [email protected] requires a peer of react@^19.0.0 but none is installed. You must install peer dependencies yourself.

(この表は npm 6・8・10 で実測しました。npm 7 系と 9 系はここでは直接確かめていませんが、厳格化そのものは npm 7 で入った挙動です。)

その結果、「手元(まだ npm 6)では入るのに、新しい npm を積んだ同僚や CI でだけ止まる」 というすれ違いが起こります。止まった環境の npm -v を先に確認してください。

「npm 6 なら入る」は「壊れていない」ではない

ここが、このエラーでいちばん誤解されやすいところです。npm 6 が警告だけで入れていたツリーは、そのまま使うと実行時に落ちることがあります。

先ほどの [email protected][email protected] を実際に同居させて、サーバレンダリング(react-dom/server)を読み込むと、返ってくるのは次のエラーです。

Error: Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:
  - react:      18.3.1
  - react-dom:  19.0.0
Learn more: https://react.dev/warnings/version-mismatch

react-dom のサーバレンダラは、読み込み時に react と同じバージョンかを検査し、ずれていればこの例外を投げます(errfix が確認したのは react-dom/server の読み込み時点です)。npm 6 が黙って入れていたのは、この読み込みで落ちるツリーでした。

react / react-dom のように実行時に版の一致を強制する組み合わせでは、npm 6 が受け入れたツリーが起動で落ちます。ERESOLVE という診断そのものは npm 7 の依存解決の変更で入ったものですが、その診断が捉えているのは、以前から実行時に潜みうる版の不整合です。ただし、すべての ERESOLVE が実行時の破綻を意味するわけではありません。 peer の指定が実態より厳しいだけで、実際には動く組み合わせもあります(次節)。この境目を踏まえると、直し方の方向を間違えずにすみます。

直し方:ぶつかっているバージョンを揃える

正しい対処は、ERESOLVE のメッセージが名指ししている2つのバージョンを、要求を満たす形にそろえることです。先ほどの例なら、reactreact-dom の求める版に上げます。

{
  "dependencies": {
    "react": "19.0.0",
    "react-dom": "19.0.0"
  }
}

この状態で npm install すると、衝突は起きずに node_modules が作られます。そして今度は react-dom を読み込んでも、バージョンが一致しているので例外は出ません。エラーを黙らせたのではなく、衝突の原因そのものを消しています。

どちらの版に寄せるかは、要求元の都合で決まります。react-dom@19 に合わせて react を上げる場合もあれば、逆に react-dom を古い版に下げて react@18 に合わせる場合もあります。判断材料は、必要な機能がどちらの版にあるかに加えて、メジャーバージョンを跨ぐときの破壊的変更や、ほかに依存しているライブラリがその版に対応しているかです。片方を動かすと別の互換性の問題を呼ぶことがあるので、そこまで見て決めます。

--legacy-peer-deps--force の何が問題か

--legacy-peer-deps は、peer dependency の指定そのものを無視して依存を展開する指定です(npm 3〜6 に相当する挙動で、peer を自動インストールもしません)。--force はこれとは別物で、衝突する peer をルートに強制的に入れることを許し、さらに既存ファイルの上書き禁止などほかの安全弁もまとめて外します。作用する範囲が広いぶん、--force のほうが危険です。どちらも ERESOLVE を消しますが、消しているのは検査であって、衝突そのものではありません。

先ほど見たとおり、react@18react-dom@19 の組み合わせは、入れても実行時に Incompatible React versions で落ちます。つまりこの組み合わせに --legacy-peer-deps を使うと、install は通るが起動で落ちる状態を買い戻すことになります。エラーが消えたぶん安心してしまい、実行してはじめて破綻に気づく、という検知の遅れやすい形です。

ただし、--legacy-peer-deps常に危険というわけではありません。ライブラリ側の peer 指定が実態より厳しすぎて、実際には問題なく動く組み合わせもあります(対応済みの上位版が出ているのに、peer の範囲がまだ更新されていない、といった場合です)。「検査を黙らせても本当に動くのか」を自分で確かめられるときに限って使う逃げ道であって、原因が分からないまま最初に打つ手とは違います。少なくとも、react / react-dom のように実行時に版一致を検査する組み合わせでは、黙らせても直りません。

切り分け(うまくいかないとき)

  • エラーの接頭辞が npm ERR! と表示される:これは npm 9 系までの書式で、npm 10 以降は npm error に変わりました(errfix は npm 8.19.4・9.9.4 で npm ERR!、10.9.4 で npm error を確認)。どちらも中身は同じ ERESOLVE unable to resolve dependency tree です。検索するときは、この接頭辞を外した部分で探すと版によらず一致します。
  • 自分では入れていないパッケージが衝突しているreact / react-dom のように直接指定した組み合わせだけでなく、あるライブラリを入れようとしたら、そのライブラリが要求する peer が既存の依存とぶつかる形でも、出るエラーは同じ ERESOLVE です。読む場所は同じで、peer ... の行が要求元を教えてくれます。揃える対象がその要求元になるだけです。
  • 要求元が第三者ライブラリで、自分ではバージョンを動かせない:この場合は「バージョンを揃える」がすぐには使えません。まず、いまの依存に対応した要求元の新しい版が無いかを確認します。無ければ、要求元に合わせて自分の依存を戻せるか、別のライブラリに替えられるか、上流の更新を待つか、を検討します。overridespackage.json、npm 8.3 以降)は依存ツリーの版を強制的に握りつぶす指定で、第三者が宣言した peer の範囲そのものを広げるものではありません。握りつぶした先が本当にその版で動くかを確認できないなら、--legacy-peer-deps と同じ「実行時に落ちる構成」を別の経路で掴むだけになります。
  • --legacy-peer-deps で入れたら動いてしまった:peer 指定が実態より厳しいだけの可能性はありますが、一度動いただけでは両立の確証にはなりません。使う API を通るテスト・本番ビルド・要求元の対応状況(issue やリリースノート)まで確認したうえで、暫定利用なら解除する条件を残しておきます。

検証(machine-verified)

この修正は node:22 のバージョン固定コンテナ内で再現し、修正後に ERESOLVE unable to resolve dependency tree のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/npm-eresolve
● reproduce ERESOLVE unable to resolve dependency tree present ✓
● apply fix exit 0
● re-run ERESOLVE unable to resolve dependency tree gone ✓
PASS verified · node:22 · signature gone