Home › JavaScript › npm Could Not Resolve Dependency? Fix ERESOLVE
Intermediate 7 min · September 23, 2026

npm Could Not Resolve Dependency? Fix ERESOLVE

Resolve npm ERESOLVE peer conflicts by upgrading the stale blocker first, pinning scoped overrides next, and deduping leftovers last..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
✓ Production
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 14 min
  • ✓Basic npm install and package.json knowledge
  • ✓Comfort reading terminal error output
  • ✓A Node project with dependencies to inspect
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • ERESOLVE means two packages demand incompatible versions of the same peer — read the conflict chain to find the blocker
  • Upgrade the outdated package first since new releases usually widen peer ranges to match the ecosystem
  • Use npm overrides for a surgical pin when upgrades can't ship today, and npm dedupe to collapse duplicates
  • For deep peer-tree forensics, see the npm-eresolve-dependency-tree guide linked in related reading
✦ Definition~90s read
What is npm Could Not Resolve Dependency Fix?

npm's ERESOLVE error fires when the dependency solver cannot satisfy every declared range simultaneously. The classic shape is a peer dependency collision: your project depends on React 18, library A declares peer react@"^18.0.0", and library B — last published two years ago — declares peer react@"^17.0.0".

★
Picture planning a group dinner where one friend only eats vegan and another booked a steakhouse.

No single React version satisfies both ^17 and ^18, so npm aborts with could not resolve dependency rather than installing a tree it knows will crash. npm 7+ enforces peers strictly; npm 6 silently skipped them, which is why old projects meet this error on their first modern install.

Reading the output is the core skill. The chain reads bottom-up: npm tries a candidate version, reports which peer requirement it violates, and names the dependent that imposed it. The package at the bottom of the chain with the narrowest, oldest range is the blocker — usually a stale library pinning a major the ecosystem has moved past.

Your options in order of durability: upgrade the blocker so its peer range overlaps the modern world; replace it with a maintained alternative; add an npm overrides entry forcing one resolved version for the subtree; or run npm dedupe to collapse duplicate copies after partial upgrades.

Escape hatches carry real costs. --legacy-peer-deps restores unenforced npm 6 behavior so builds pass while incompatible copies coexist until a runtime crash. --force checks even less. Overrides in package.json are stricter and reviewable — they pin a resolution explicitly — but they shift maintenance burden onto you at every upgrade.

For tangled multi-package trees, the companion npm-eresolve-dependency-tree guide covers full arborist forensics; this article solves the common single-blocker case.

Plain-English First

Picture planning a group dinner where one friend only eats vegan and another booked a steakhouse. Both demands are valid alone, but together no restaurant satisfies everyone — so the booking fails. That's an npm peer conflict. Two packages you depend on each insist on a different version of a shared third package, and npm refuses to install a combination it knows will break. The fix isn't forcing the booking; it's updating one friend's constraints or picking a compromise everyone accepts.

You run npm install and instead of a clean tree you get a wall of ERESOLVE text: could not resolve dependency, peer react@"^17.0.0" from some-library, conflicting peer react@"^18.0.0" from another. The output names five packages and three version ranges, and every suggestion online says --legacy-peer-deps, which feels like typing sudo to silence a warning. It installs — and your app crashes at runtime with a hooks error that takes a day to trace.

Peer conflicts are npm keeping a promise: it won't assemble a dependency tree it can prove is broken. The conflict output is a diagnosis, not noise. It names the exact packages whose requirements collide and the ranges that don't overlap. Learning to read that chain turns a 30-minute panic into a 5-minute decision between upgrading the blocker, pinning an override, or replacing the stale package.

This guide teaches the read-first workflow: find the blocker, upgrade it, override surgically when you can't, and dedupe the result. You'll also learn why --legacy-peer-deps is a loan with runtime interest, and when the deep-dive companion guide earns its click.

Reading the Conflict Chain: Find the Blocker

ERESOLVE output reads like a stack trace: start at the bottom. npm names the package it tried to place, the peer range that candidate violates, and the dependent imposing that range. Walk down until you find the package with the oldest publish date and the narrowest range — typically a library pinning a previous major (react@^17 while the world ships 18). That single package usually explains the entire wall of text above it; everything else is collateral.

Confirm with the registry before deciding. npm view <pkg> versions shows whether a newer release exists; npm view <pkg>@latest peerDependencies shows whether it widened its range. If the newest release still pins the old major, the package is abandoned and replacement beats upgrade. Spending ten minutes reading the chain saves hours of flag-and-pray cycles — the blocker is almost always one stale dependency, not a systemic ecosystem failure.

npm explain is the magnifying glass for confirmed blockers. Run npm explain <package> to see every path through which it enters the tree — direct, transitive, peer — with the versions each parent requests. When the same package appears under three parents with three ranges, the narrowest oldest one is your upgrade target and the others are context. Combine with npm view time data (npm view <pkg> time --json shows release dates) to judge maintenance health: a blocker untouched for two years won't widen its peers next week, so replacement planning starts now. Save the full ERESOLVE output plus explain traces in the upgrade ticket; the next engineer inherits evidence instead of archaeology. Reading chains carefully resolves most walls to one stale package — and the ticket proves it wasn't guesswork.

BASH
1
2
3
4
npm install 2>&1 | tail -40
npm view ui-date-picker versions --json | tail -5
npm view ui-date-picker@latest peerDependencies
npm ls react --all | head -20
📊 Production Insight
A 60-line ERESOLVE wall traced to one date picker pinning React 17. Upgrading that single package cleared the entire chain — the other 55 lines were downstream noise.
🎯 Key Takeaway
Read bottom-up to the oldest narrow range. One stale blocker usually explains the whole conflict output.

Upgrade the Blocker: The Fix That Stays Fixed

When the blocker has a newer release supporting your major, upgrade it. This is the only fix that removes the conflict instead of suppressing it: the new peer range overlaps your tree, strict install passes, and future npm operations stay clean. Budget for API renames honestly — major bumps earn their peer widening with breaking changes, so codemod call sites in the same PR and run the full suite before merging.

Verify the upgrade actually unified the tree. npm ls <singleton> must show exactly one copy; du -sh node_modules before and after quantifies the dedupe win. If the upgrade introduces its own conflicts, you're peeling a stale layer cake — upgrade the next blocker the same way. Two or three iterations retire years of drift. For genuinely tangled nested peer trees where the chain loops through multiple packages, the companion npm-eresolve-dependency-tree guide covers full arborist forensics step by step.

Codemods make major bumps shippable inside one sprint. Most popular libraries ship migration guides with rename mappings; jscodeshift scripts apply them across hundreds of call sites in minutes, and the full suite confirms behavior preservation. When no codemod exists, wrap the migration: introduce the new version behind a compatibility shim in one PR, migrate call sites incrementally, then delete the shim — each step stays green and reviewable. Budget suite time honestly: peer upgrades touch shared components, so run the complete test matrix including integration and visual tests, not just unit scope. Verify the unified tree afterward with npm ls on every singleton your app depends on (react, styled-components, state managers), because a second duplicate hiding behind the first fix wastes the whole exercise. Two or three blocker upgrades retire years of drift and unblock every future bump.

BASH
1
2
3
4
npm install -S ui-date-picker@^4.1.0
npm ci
npm ls react --depth=0
du -sh node_modules
📊 Production Insight
Upgrading one picker across 23 call sites deleted the CI bypass flag, unified React to a single copy, and shrank node_modules by 310MB after dedupe.
🎯 Key Takeaway
Upgrade the stale package first. It's the only fix that deletes the conflict instead of hiding it.

Overrides: Surgery When You Can't Upgrade Today

Sometimes the upgrade can't ship this week — a rename touches 200 files, or the release train leaves Friday. npm overrides let you pin a resolution explicitly: "overrides": { "react": "^18.2.0" } forces every subtree to resolve React 18 regardless of declared peers. Unlike command-line flags, overrides are version-controlled, reviewable, and visible to every install — the conflict decision lives in the repo, not in someone's shell history.

Use them narrowly and temporarily. Over-scope the pin and you'll force versions onto packages that genuinely need the old one, trading a solver error for runtime breakage. Scope to the subtree when possible ("overrides": { "ui-date-picker": { "react": "^18.2.0" } }) and attach the upgrade ticket as a code comment. Revisit quarterly: each override is maintenance debt that can block future upgrades exactly like the stale peer it bypassed.

Version-control the reasoning, not just the pin. Every overrides entry deserves an inline comment with the blocked upgrade ticket, the date added, and the removal condition (blocker reaches version X) — future upgrades then read intent instead of guessing whether the pin is still load-bearing. Review overrides in dependency-update PRs explicitly: Renovate and Dependabot surface new blocker releases, and each one is a chance to delete a pin. Measure override scope with npm ls before and after to confirm the pin affects only the stale subtree; an over-broad pin that drags unrelated packages along shows up as unexpected version shifts in the lockfile diff. Teams that calendar quarterly override reviews delete most pins within two quarters. The ones that skip reviews accumulate pins that eventually conflict with each other — overrides fighting overrides, with the solver caught in the middle.

package.json (excerpt)JSON
1
2
3
4
5
6
7
8
9
10
11
{
  "dependencies": {
    "react": "^18.2.0",
    "ui-date-picker": "^3.2.1"
  },
  "overrides": {
    "ui-date-picker": {
      "react": "^18.2.0"
    }
  }
}
⚠ Overrides Can Break the Package They Pin
Forcing a version the library never tested against may crash at runtime. Prefer upgrading the library; use overrides as a dated bridge with an owner and a removal ticket.
📊 Production Insight
A scoped override bridged a 3-week gap to a planned upgrade. The ticket reference in the comment got it removed on schedule — unscoped overrides elsewhere lingered for a year.
🎯 Key Takeaway
Pin overrides narrowly, comment the expiry ticket, and remove them when the real upgrade lands.

npm dedupe: Collapse the Leftovers

Partial upgrades leave duplicate transitive copies scattered through the tree — same library at two versions, both technically satisfying their parents. npm dedupe re-solves placement to hoist the maximum shareable set, collapsing duplicates whose ranges overlap. It's the cleanup pass after the real fix, not a fix itself: dedupe can't merge truly disjoint ranges, so run it after upgrading the blocker, not instead of it.

Measure before and after. npm ls <name> --all shows the duplicate set; du -sh node_modules quantifies the bloat (hundreds of megabytes is common in flag-era trees). Commit the resulting lockfile so CI reproduces the collapsed tree exactly. If dedupe reports it can't hoist, read which ranges stay disjoint — that's your next blocker to upgrade, identified for free.

Lockfile discipline makes dedupe results stick. Always run dedupe with the lockfile present and commit the result in the same PR as the upgrade, so CI's npm ci reproduces the collapsed tree exactly — dedupe without a committed lockfile collapses locally and diverges remotely. Review the lockfile diff for surprise removals: dedupe occasionally drops a nested copy some deep consumer relied on implicitly, and the suite (not the solver) is the backstop that catches it. For monorepos, run dedupe per workspace plus at the root; workspace boundaries hoist differently than flat trees, and a root-only pass leaves workspace duplicates in place. Track node_modules size and install duration as pipeline metrics — sudden growth after a dependency PR signals new duplicates worth collapsing. Dedupe is maintenance, not magic: schedule it after every upgrade PR and the tree stays lean instead of accumulating flag-era bloat.

BASH
1
2
3
4
npm ls lodash --all | head -30
npm dedupe
npm ls lodash --all | head -10
du -sh node_modules
📊 Production Insight
Post-flag cleanup via dedupe removed 4 duplicate transitive trees and 310MB from node_modules. Install times dropped from 6 minutes to under 3.
🎯 Key Takeaway
Run dedupe after upgrading, not instead of it. It merges overlapping ranges; disjoint ones still need the upgrade.

Why --legacy-peer-deps Is a Loan With Interest

The flag restores npm 6 semantics: peers install unenforced, the solver stays quiet, and CI goes green. The debt comes due at runtime. Unchecked peers let incompatible copies coexist — duplicate React breaking hooks, duplicate styled-components splitting themes, duplicate state managers forking stores. Each failure looks like an app bug, complete with misleading stack traces, and none of them reproduce in the solver output you silenced.

If you must use it as a Friday-night bridge, contain it: apply to one install, file the upgrade ticket in the same hour, and add a CI check that fails on the flag's presence so it can't become permanent. Audit existing repos for .npmrc files carrying legacy-peer-deps=true — silent repo-level flags are how teams forget they're bypassing the solver for months. The flag's honest name would be --break-quietly-later.

Detecting silent bypasses requires active hunting. Grep workflows, Dockerfiles, .npmrc files (repo, home, and CI-global), and package.json scripts for legacy-peer-deps and --force — each hit is an install the solver isn't checking. npm config list surfaces effective settings including global and environment-sourced flags that per-file greps miss. Replace each bypass with a dated plan the same week: upgrade ticket for supported blockers, scoped override with expiry for blocked ones, replacement spike for abandoned packages. Add the CI gate from the incident (fail on flag presence) so removed bypasses stay removed — without the gate, the next Friday-night deploy re-adds the flag and the cycle restarts. Report bypass count as a health metric in dependency reviews; zero is the only acceptable steady state, and every temporary bridge should name the engineer responsible for burning it down.

BASH
1
2
3
grep -rn 'legacy-peer-deps\|force' .github/workflows/ .npmrc ~/.npmrc package.json 2>/dev/null
cat .npmrc 2>/dev/null || echo 'no repo npmrc'
npm ls react --depth=0
📊 Production Insight
One CI flag hid a React duplication that crashed 40% of dashboard sessions (900 reports in 6 hours). A one-line grep gate in CI now blocks the flag permanently.
🎯 Key Takeaway
Bypass flags move failures from install time to runtime. Gate them out of CI and treat any use as a dated emergency bridge.

When to Open the Deep-Dive Companion Guide

Most conflicts end at the blocker upgrade. Open the npm-eresolve-dependency-tree deep dive when the chain loops through three or more packages, when overrides cascade (fixing one conflict reveals another), or when monorepo workspace ranges interact with root pins. The companion covers npm arborist internals: reading ideal-tree reification logs, using npm explain to trace why a version was chosen, and structuring workspace peer ranges so siblings don't fight.

Bring evidence when you go: the full ERESOLVE output, npm explain <pkg> for each disputed copy, and your lockfile diff. Deep forensics without the explain output is guesswork. And keep this article's order of operations — blocker, upgrade, override, dedupe — as the first pass even on tangled trees. Half of supposedly deep conflicts still resolve to one abandoned package once you read the chain carefully.

Preparing evidence before escalating saves the deep-dive session. Capture the complete ERESOLVE output (not excerpts — the chain head matters), npm explain for each disputed copy, npm ls --all for the full tree, and the lockfile diff of the change that introduced the conflict. Note which upgrades you already attempted and their outcomes; the companion guide's forensics assume the single-blocker pass is done. In the session, work bottom-up through nested chains one collision at a time, pinning each resolution with overrides as you go so partial progress commits incrementally. Time-box the forensics: if three sessions haven't untangled the tree, the answer is usually replacing the worst offender with a maintained alternative rather than solving a five-body problem. Evidence-first escalation resolves most tangles in one sitting — the logs do the talking and the team stops guessing.

BASH
1
2
3
npm explain react 2>&1 | head -30
npm install 2>&1 | tail -40
npm ls --all 2>&1 | grep -c 'deduped' || true
📊 Production Insight
A team escalated to arborist forensics with logs in hand and resolved a 4-package loop in one session. Evidence-first debugging beat two prior days of flag guessing.
🎯 Key Takeaway
Solve the single-blocker case here first. Escalate to the deep dive with full logs only for multi-package loops.
● Production incidentPOST-MORTEMseverity: high

A Stale Date Picker Blocked All Deploys for 5 Hours

Symptom
After upgrading the app to React 18, npm ci failed in CI with ERESOLVE: peer react@"^17.0.0" from ui-date-picker@3.2.1 conflicting with react@18.2.0. To unblock Friday deploys, an engineer added --legacy-peer-deps to the CI install step. Builds went green. On Monday, 40% of dashboard sessions crashed with invalid hook call warnings — duplicate React copies (17 nested under the picker, 18 at top level) broke hooks identity. Support logged 900 crash reports in 6 hours before the install flag was connected to the runtime errors, and all deploys froze for 5 hours during the revert.
Assumption
The team assumed peer warnings were advisory and the flag only affected install tooling. They also assumed two React copies would resolve to one at bundle time — webpack instead bundled both, 140KB extra, with state split across copies. A second assumption compounded it: that pinning the flag in CI was equivalent to resolving the conflict, so nobody scheduled the picker upgrade. The runtime crash looked like an app bug, and two engineers profiled components for a day before checking npm ls react.
Root cause
ui-date-picker@3.2.1 declared peer react@"^17.0.0" and had no React 18-compatible release on its 3.x line; version 4.0 with ^18 peers existed but required prop renames. --legacy-peer-deps installed React 17 nested under the picker alongside top-level React 18. Hooks called inside picker components bound to the nested copy while the app used the top-level one, violating React's single-copy invariant. The install succeeded by definition — the flag tells npm to stop checking — and the failure moved to runtime where it was harder to diagnose.
Fix
The picker was upgraded to 4.1.0 across 23 call sites (prop renames codemodded in one commit), the CI flag was deleted, and npm ci passed strict on the first try. An overrides entry was considered and rejected since the maintained upgrade existed. npm dedupe collapsed 4 duplicate transitive copies left over from the flag era, shrinking node_modules by 310MB. A CI gate now fails the build if --legacy-peer-deps or --force appears in any workflow, and npm ls react runs in the pipeline asserting a single copy.
Key lesson
  • Install-time strictness exists to prevent runtime crashes. Every bypass flag converts a 5-minute solver error into a multi-hour production debug with user impact.
  • Duplicate React is never benign. Assert a single copy with npm ls react in CI — hooks identity depends on module identity, and two copies always break eventually.
  • Gate against the escape hatches themselves. A one-line CI check for legacy-peer-deps would have blocked the flag PR and forced the real upgrade on Friday.
Production debug guideFive steps that shrink the wall of text to one blocker and one decision.5 entries
Symptom · 01
npm install fails with ERESOLVE and names peer ranges
→
Fix
Read bottom-up: run npm install 2>&1 | tail -40 to capture the full chain, then identify the lowest package with the oldest narrow range — that's the blocker. Check its latest version with npm view <blocker> versions --json | tail -5 and whether the newest release widens peers via npm view <blocker>@latest peerDependencies.
Symptom · 02
You suspect duplicate React or duplicate copies of a singleton
→
Fix
Prove it with npm ls react (or the singleton name) and du -sh node_modules to size the damage. Two listed copies confirm the hooks-breaker. Remove bypass flags, upgrade the stale dependent, and re-run npm ls to assert exactly one copy remains.
Symptom · 03
Upgrade of the blocker needs prop/API renames you can't ship today
→
Fix
Pin surgically with an overrides entry: add "overrides": { "react": "^18.2.0" } to package.json, run npm install, and verify with npm ls react. Leave a TODO with the upgrade ticket number — overrides are a bridge, and every one needs an expiry owner.
Symptom · 04
Tree has leftover duplicates after partial upgrades
→
Fix
Collapse them with npm dedupe then re-verify via npm ls <name> --all | head -30. If dedupe can't unify (ranges truly disjoint), the blocker still needs upgrading — dedupe only merges ranges that already overlap.
Symptom · 05
CI passes but a teammate's laptop fails (or vice versa)
→
Fix
Diff environments: run npm -v && node -v on both, check for .npmrc files with legacy-peer-deps=true via cat .npmrc ~/.npmrc, and confirm both use npm ci from the same lockfile. Delete stray flags so every machine solves identically.
Could Not Resolve Dependency — Options Compared
Root CauseHow to ConfirmFixPrevention
Stale blocker pins old majorBottom of chain; old peer rangeUpgrade the blockerRenovate plus CI strict install
Upgrade blocked this weekRename scope too large for trainScoped overrides bridgeTicketed expiry with owner
Leftover duplicate copiesnpm ls shows two versionsnpm dedupe plus lockfileSingle-copy CI assertion
Bypass flag in CI or npmrcgrep finds legacy-peer-depsDelete flag; fix properlyCI gate failing on flags
Abandoned package, no releaseNo new version in registryReplace with maintained forkHealth-check deps quarterly
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
npm install 2>&1 | tail -40Reading the Conflict Chain
npm install -S ui-date-picker@^4.1.0Upgrade the Blocker
package.json (excerpt){Overrides
npm ls lodash --all | head -30npm dedupe
grep -rn 'legacy-peer-deps\|force' .github/workflows/ .npmrc ~/.npmrc package.js...Why --legacy-peer-deps Is a Loan With Interest
npm explain react 2>&1 | head -30When to Open the Deep-Dive Companion Guide

Key takeaways

1
Read ERESOLVE bottom-up; one stale blocker explains most walls.
2
Upgrade the blocker
the only fix that deletes the conflict.
3
Scope overrides narrowly with a ticketed expiry date.
4
Dedupe after upgrading and commit the resulting lockfile.
5
Assert single copies of singletons in CI on every change.
6
Gate bypass flags out of CI; never let them become permanent.

Common mistakes to avoid

6 patterns
×

Adding --legacy-peer-deps as the first response

Symptom
Install passes; weeks later duplicate-singleton crashes appear with misleading stacks.
Fix
Read the chain, upgrade the blocker, and reserve flags for dated emergencies with a removal ticket.
×

Pinning overrides repo-wide instead of per-subtree

Symptom
Unrelated packages break at runtime against a version they never supported.
Fix
Scope overrides to the stale subtree and comment the upgrade ticket inline.
×

Running npm install in CI instead of npm ci

Symptom
Lockfile drifts per run; conflicts appear on some machines only.
Fix
Use npm ci everywhere automated so every machine solves the identical tree.
×

Skipping npm ls verification after the fix

Symptom
Duplicate React ships silently and hooks break for a subset of users.
Fix
Assert a single copy of singletons in CI after every dependency change.
×

Leaving legacy-peer-deps=true in .npmrc

Symptom
Solver stays bypassed for months; nobody remembers the check is off.
Fix
Delete repo-level bypass flags and gate CI against their return.
×

Upgrading without running dedupe and committing the lockfile

Symptom
Duplicates linger, installs stay slow, and CI resolves a different tree.
Fix
Run npm dedupe, verify with npm ls, and commit the lockfile.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does ERESOLVE actually mean, in one minute?
Q02SENIOR
Why is --legacy-peer-deps dangerous?
Q03SENIOR
When are npm overrides the right call?
Q04SENIOR
npm ls shows two copies of React. How did that happen and how do you fix...
Q05SENIOR
How do overrides differ from --legacy-peer-deps?
Q01 of 05JUNIOR

What does ERESOLVE actually mean, in one minute?

ANSWER
The solver can't satisfy all declared ranges at once — usually two packages demanding different majors of one peer. I'd read the chain bottom-up to the oldest narrow range and upgrade that blocker.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Should I use --force instead?
02
Why did npm 6 projects start failing on npm 7+?
03
Do overrides affect transitive dependencies?
04
How do I know which package is the blocker?
05
Can dedupe fix ERESOLVE by itself?
06
Where's the deep-dive guide for tangled trees?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Written from production experience, not tutorials.

Follow
✓ Verified
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
🔥

That's Node.js. Mark it forged?

7 min read · try the examples if you haven't

←
Previous
ERR OSSL EVP Unsupported Fix
28 / 30 · Node.js
Next
npm EACCES Permission Denied Fix
→