npm ERR! ERESOLVE: Fix Dependency Conflicts Fast
npm ERESOLVE means npm v7+ found conflicting peer dependencies, so read the report, run npm ls, and add overrides.
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
- ✓Node.js with npm v7 or newer installed and checkable
- ✓A project with package.json plus package-lock.json
- ✓Basic comfort reading semver ranges like ^18.0.0
- Read the report first:
While resolving Xnames the requester,Found Ynames the installed copy,Could not resolvenames the clash. - Run
npm lsto see which parents pull in each duplicate, thennpm dedupeto collapse copies that semver allows. - Pin one copy with the
overridesfield in package.json (e.g. react@18.2.0), then runnpm installto rebuild the lockfile. - Treat
--legacy-peer-depsas a temporary unblock only: it ignores peer clashes and can ship two Reacts or a broken plugin. - Commit package-lock.json and prove the fix with
npm ci— never delete node_modules as a first step.
Imagine two wedding guests who each demand the only window seat at the same table — guest A will not sit unless the seat is hers, guest B will not sit unless it is his, and there is one chair. npm v7 is the planner who stops the wedding instead of squeezing both in. Older npm stacked them together and hoped, which is why apps shipped two React copies with bizarre bugs. ERESOLVE hands you the conflict card: read who wants what, pick one arrangement with overrides, and rewrite the seating chart.
You run npm install after adding one harmless package, and instead of a progress bar you get a wall of red: npm ERR! ERESOLVE could not resolve, While resolving X, Found Y, Could not resolve dependency. Nothing you wrote is wrong, yet the install refuses to proceed. If you have shipped JavaScript since npm 7, you have stared at this report — and if you have not, your first encounter is only one dependency away.
The frustration is understandable, but the error is doing you a favor. Before npm 7, conflicting peer demands were waved through with a warning, and apps shipped with two copies of React or a date picker wired to the wrong version. Those installs succeeded and then broke at runtime in ways nobody could trace. ERESOLVE fails loudly at install time so you fix the wiring before your users find it.
This guide teaches you to read the report like a work order: who demands what, which copies collide, and which fix fits. You will learn the npm ls plus dedupe workflow for mapping conflicts, the overrides field for pinning transitive copies, and the honest risks of --legacy-peer-deps. You will also learn the lockfile discipline that keeps conflicts fixed — including why deleting node_modules is the last resort, not the first step.
Why npm v7 Turned Peer Clashes Into Hard Errors
Peer dependencies are a package's way of saying I need to share one copy of something with your app. A plugin does not bundle its own React — it declares react as a peer and expects your project to provide exactly one. That design prevents duplicate stateful libraries, which break hooks, contexts, and singletons. Through npm 6, the installer treated peer clashes as advisory: it printed a warning, installed whatever fit best, and moved on. Most teams never read those warnings, so applications routinely shipped with doubled libraries and mystery runtime bugs.
npm 7 replaced the installer with arborist, a resolver that builds the entire dependency tree as one constraint problem before downloading anything. Every peer range becomes a hard requirement, and when no single version satisfies all of them, the install stops with ERESOLVE instead of guessing. This strictness felt like a regression to teams upgrading npm — installs that worked for years suddenly failed — but it converted silent runtime corruption into loud install-time failures. A red build you can read beats a green build that crashes in production.
The practical consequence is that peer ranges now deserve the same respect as your own dependencies. Before adding any package, inspect its peer demands with npm view and compare them against your installed majors. If a plugin wants React 17 and you run 18, you have three honest options: upgrade the plugin, downgrade your app (rarely wise), or pin the tree with overrides after verifying compatibility. What you cannot do anymore is ignore the mismatch and hope — arborist removed hope from the process, and your installs are better for it.
Reading the ERESOLVE Report Line by Line
An ERESOLVE report looks intimidating, but it contains exactly three facts plus an advertisement. Fact one sits on the While resolving line: it names the package whose demand could not be satisfied, often your root project or a plugin you just added. Fact two sits on the Found lines: they list the versions already placed in the tree, with their locations — top level versus nested under some parent. Fact three sits on the Could not resolve line: it states the required range that nothing in the tree meets. Everything after that, including the suggestion to retry with --legacy-peer-deps, is npm's generic escape hatch, not a recommendation.
Read a React 17 versus 18 clash through this lens and it becomes a work order. While resolving: your-app names your project as the requester. Found: react@18.2.0 at the top level shows what you run. Found: react@17.0.2 nested under the analytics package shows the intruder. Could not resolve: react@17.x from the analytics plugin states a demand your tree cannot meet without demoting your whole app. The fix space is now obvious and small: make the plugin accept 18 (upgrade it), force one copy with overrides, or remove the plugin. None of that requires touching node_modules.
Build one reading habit and most reports surrender in minutes: scan upward from the bottom until you find the first While resolving line, then read downward through its Found and Could not resolve lines as a group. Reports can chain several groups when multiple clashes exist — resolve them one group at a time, starting with the shared libraries (react, typescript, eslint) whose duplicates cause the worst runtime damage.
npm ls and npm dedupe: Mapping the Conflict
Once the report names the clash, npm ls draws you a map of it. Running npm ls with the conflicted package name prints every installed copy and, critically, the chain of parents above each one — so you can see that react@18.2.0 sits at the top level while react@17.0.2 hides two levels deep under the analytics plugin. That parent chain is the whole game: it tells you whether the fix belongs in your direct dependencies or in an overrides entry aimed at a transitive one. Run npm ls before every fix attempt and after, so you have a before-and-after picture instead of vibes.
npm dedupe plays the supporting role. After hoisting analysis, npm can often collapse duplicate minor versions into one shared copy when their ranges overlap — three nested copies of a utility at 4.17.20 and 4.17.21 become one. Run it, then re-run npm ls to see what survived. Whatever remains after dedupe represents genuinely incompatible demands (different majors, disjoint ranges) that no hoisting trick can merge. That boundary matters because it tells you when to stop tidying and start deciding: upgrade a package, pin with overrides, or replace the offender.
Two companion commands round out the mapping kit. npm view with the peerDependencies field shows what any published version demands without installing it — perfect for checking whether a newer plugin release already supports your major. npm outdated flags how far your pins lag, which hints whether an upgrade path even exists. Together these turn dependency archaeology from guesswork into a fifteen-minute routine: map with ls, tidy with dedupe, research with view, and only then edit package.json.
The overrides Field: Pinning Your Way Out
The overrides field is package.json's way of settling transitive disputes by fiat: it forces one version of a package across your entire tree, no matter what nested dependencies request. When an analytics plugin demands react@17.x and your app runs 18.2.0, an override pinning react to 18.2.0 tells arborist to place that copy everywhere and skip the unsatisfiable demand. One edit, one npm install to rebuild the lockfile, and the resolver that failed for hours goes green. The snippet below shows the shape — an overrides block keyed by package name with the exact version you have already tested.
Overrides demand respect because they override compatibility claims you did not write. Pinning React 18 under a plugin built for 17 can work flawlessly or break subtly, so verify like you mean it: run the full test suite, exercise the plugin's UI paths in staging, and confirm with npm ls that exactly one copy remains. Prefer upgrading the requesting package first — a plugin release whose peer range already includes your major needs no forcing at all. Reach for overrides when the upgrade does not exist yet, the maintainer is slow, or the old peer range is needlessly narrow for code that works fine.
Keep overrides visible and temporary where possible. Comment each entry with why it exists and which upstream release would let you remove it, and re-check on every dependency upgrade pass. An overrides block with no comments becomes permanent furniture that nobody dares touch — the same trap as a committed --legacy-peer-deps flag, just better dressed. Review them quarterly: the best override is the one you delete because the ecosystem caught up.
--legacy-peer-deps: What It Hides and When It Is Safe
The --legacy-peer-deps flag tells npm to resolve peers the way npm 6 did: install through the clash, print warnings nobody reads, and hope the tree works. As a local, temporary unblock it has legitimate uses — you need a build running this afternoon while the real fix waits on a plugin release. The danger starts when temporary becomes permanent: committed into .npmrc, baked into Dockerfiles, pasted into runbooks. At that point every future peer clash across every dependency installs silently, and your team loses the exact protection arborist was built to provide.
What it hides is concrete and nasty. Duplicate copies of stateful libraries — two Reacts, two emotion instances, two router contexts — where hooks fire against the wrong copy and contexts read as empty. A plugin wired to an incompatible major that throws only on the one code path your tests skip. None of this appears at install time because you disabled the check that would have named it. The Tuesday incident is the textbook case: the flag produced a green laptop install with react 17 nested under the plugin, and staging crashed with invalid hook calls that took longer to diagnose than the original conflict.
If you must use it, fence it in. Pass it on the command line for one install, never commit it, and open the tracking ticket in the same hour with the overrides-or-upgrade plan attached. Grep the repo for it in CI and fail builds that smuggle it into .npmrc without an expiry comment. And remember its honest alternative: --strict-peer-deps, which turns even warnings into errors so new clashes surface in pull requests instead of release branches.
Lockfile Discipline: Why Deleting node_modules Comes Last
The lockfile is the only record of the tree that actually worked, so treat it as a first-class artifact. package-lock.json pins every transitive version, which means npm ci on any machine rebuilds the identical tree that passed your tests. That determinism is exactly what ERESOLVE fixes need: after overrides or upgrades resolve the clash, npm install writes the settlement into the lockfile, you commit it, and every teammate plus CI installs the settled tree instead of re-litigating the conflict. A fix that is not in the lockfile is a rumor, not a resolution.
This is also why deleting node_modules fixes nothing. The conflict lives in version requirements — ranges in package.json and pins in the lockfile — while node_modules holds only downloaded files. Wiping the folder and reinstalling replays the same resolver against the same requirements and fails identically, ten minutes later. Reserve deletion for its real uses: corrupted downloads, phantom binaries after an interrupted install, or native modules built against the wrong Node version. Those are file problems with file solutions; ERESOLVE is a requirements problem with a requirements solution.
Build the discipline into your workflow and conflicts stop recurring. Commit the lockfile for every application (libraries may differ, apps must not). Prove fixes with rm -rf node_modules plus npm ci on a clean checkout before merging. Run npm ci — never bare npm install — in CI so a stale lockfile fails loudly instead of being silently rewritten. The teams that follow this routine spend dependency-upgrade days reviewing lockfile diffs calmly; everyone else spends release days re-running installs hopefully.
The Tuesday Build That Went Red Over a React Peer Clash
overrides entry pinned react to 18.2.0 across the tree and npm install rebuilt the lockfile — npm ls react showed exactly one copy and CI went green in 9 minutes. Second, --legacy-peer-deps was removed from .npmrc and the analytics package was upgraded two minor releases to one whose peer range officially supports React 18, letting the override retire the next sprint. Third, CI gained an npm ci step on a clean checkout plus an npm ls react assertion, so a duplicate React fails the pull request instead of the release branch.- Read the resolver output before re-running the command. ERESOLVE names the requester, the installed copy, and the clash in its first lines. Four blind re-installs burned an hour; one careful read would have pointed at the analytics package in five minutes.
- Convenience flags are debt with interest. --legacy-peer-deps bought a green local install and paid for it with a staging runtime crash nobody could trace. If a flag suppresses a safety check, the suppression needs an owner, a ticket, and an expiry date.
- Assert your dependency invariants in CI, not in memory. A one-line npm ls assertion for singleton packages like react turns next quarter's peer clash into a failed pull request instead of a blocked release.
npm install 2>&1 | tee /tmp/eresolve.log, then read the full report — not just the last line. Find the While resolving X line (the requester), the Found: Y lines (installed copies), and the Could not resolve line (the unsatisfiable demand). Those three lines are the entire work order; everything below is the suggested escape hatch, not the diagnosis.npm ls react (substituting the conflicted package) to print every copy in the tree and which parent pulls it in. Then run npm view <parent> peerDependencies for the top suspect to see the exact range it demands. If the tree shows 18.2.0 at top level and 17.0.2 nested under one plugin, you have a transitive clash and the fix is overrides, not an upgrade of your app code.npm dedupe and then npm ls <pkg> again. Dedupe hoists shared copies and collapses duplicates whose ranges overlap, which clears minor-version clutter for free. If two majors remain afterward, dedupe has done all it can — the leftover pair needs overrides or an upgrade, and no further dedupe runs will change that.overrides entry pinning the shared package, run npm install, and review the lockfile diff for sanity. Then prove the tree is clean: npm ls <pkg> must show exactly one version, and rm -rf node_modules && npm ci on a fresh checkout must pass. If CI still fails here, the committed lockfile is stale — rebuild and commit it.npm --version on both machines and compare majors — npm 6 waves through clashes that npm 9 refuses, so version skew alone explains most cannot-reproduce-it cases. Standardize on one npm major via the Node image or packageManager field, delete the flag from .npmrc with grep -rn legacy-peer-deps .npmrc .npmrc* 2>/dev/null, and re-resolve with the standard version.| File | Command / Code | Purpose |
|---|---|---|
| check-resolver.sh | npm --version | Why npm v7 Turned Peer Clashes Into Hard Errors |
| map-conflict.sh | npm ls react | npm ls and npm dedupe |
| package.json | { | The overrides Field |
| legacy-flag-check.sh | npm install --legacy-peer-deps | --legacy-peer-deps |
| lockfile-discipline.sh | npm install | Lockfile Discipline |
Key takeaways
Common mistakes to avoid
6 patternsDeleting node_modules as the first step
npm install 2>&1 | tee /tmp/eresolve.log, then identify the requester, the installed copy, and the clash. Only then run npm ls <pkg> to map the conflict. Diagnosis takes five minutes; blind flag-adding takes hours.Committing --legacy-peer-deps into .npmrc permanently
--legacy-peer-deps as a local, temporary unblock and track the real fix in the backlog the same day. If CI needs it to stay green, that is a signal to schedule the overrides or upgrade work now, not to commit the flag.Ignoring peer warnings until they become errors
npm view <pkg> peerDependencies to see what the package actually demands, then satisfy it with overrides or an upgrade. Treat peer warnings as errors in CI with --strict-peer-deps so new clashes surface at PR time.Hand-editing package-lock.json to force a resolution
npm install silently rewrites your manual edit, the conflict returns, and the diff is unreadable. Worse, a hand-edited lockfile can describe a tree npm itself would never build, so teammates get different installs from the same commit.npm install, then review the lockfile diff. The lockfile is generated output — your edits belong in package.json.Mixing npm versions across the team
npm --version assertion or a pinned Node image), and use overrides for transitive clashes rather than flags that differ by version. Verify with npm ci on a clean checkout before merging.Upgrading only the top-level package and leaving the transitive clash
overrides to pin the transitive copy to the version your app already uses (e.g. react@18.2.0), or upgrade the requesting package to a release with wider peer support. Then confirm with npm ls react that exactly one copy remains.Interview Questions on This Topic
Why did ERESOLVE appear after upgrading npm?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
That's Node.js. Mark it forged?
7 min read · try the examples if you haven't