Quick answer
npm ERESOLVE means npm cannot select versions that satisfy every dependency and peer-dependency constraint. Read the Found, Could not resolve dependency, and peer lines as one conflict. Then align the host and plugin versions, update or replace the incompatible package, regenerate the lockfile deliberately, and verify with npm ci. Do not treat legacy-peer-deps or force as permanent fixes.
The error looks like a package-manager problem:
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency treeIt is actually npm refusing to make a promise it cannot keep. Your application, its direct dependencies, and their peer dependencies describe a set of version constraints. At least two of those constraints have no compatible version in common.
The popular answer—npm install --legacy-peer-deps—often makes the command
return zero. It also tells npm to stop enforcing the peer contract that exposed
the problem. That can be an acceptable, short-lived diagnostic choice. It is
not the same as resolving the dependency graph.
This guide shows how to read the report, choose a real compatibility strategy, avoid lockfile folklore, and keep the repaired tree stable across local development, Docker, workspaces, CI, and production builds.
Quick Answer
npm ERESOLVE means npm cannot select versions that satisfy every dependency
and peer-dependency constraint. Read the Found, Could not resolve dependency, and peer lines as one conflict. Then align the host and plugin
versions, update or replace the incompatible package, regenerate the lockfile
deliberately, and verify with npm ci. Do not treat legacy-peer-deps or
force as permanent fixes.
TL;DR
ERESOLVEis a version-constraint conflict, usually a peer dependency mismatch—not a corruptednode_modulesdirectory.- Identify the host version npm found, the package that requires a peer, and the peer range it requires.
- Prefer a compatible plugin release, then a supported host framework version, then a maintained replacement.
npm installupdates the lockfile after a deliberate manifest change;npm civerifies the exact committed tree in a clean environment.--legacy-peer-depsignores peer contracts.--forcebypasses safeguards. Neither proves runtime compatibility.- Use
overridesonly for reviewed transitive version policy; it does not rewrite a package's peer API support. - A cache clean, deleting the lockfile, or repeatedly reinstalling does not reconcile incompatible semver ranges.
Symptoms
| Symptom | Meaning | Next check |
|---|---|---|
ERESOLVE unable to resolve dependency tree | npm could not find one valid install graph | Read the first peer conflict |
Found: react@19.x | npm selected or was asked for this host version | Check the root manifest and lockfile |
peer react@"^18" from package-x@… | A plugin only declares React 18 compatibility | Find a plugin release that supports React 19 |
Conflicting peer dependency: … | npm shows a candidate that satisfies one side, not both | Compare both ranges, not just package names |
npm ci fails but npm install works | Lockfile and manifest/configuration differ | Compare the lockfile and install flags |
| Warning or error only in one workspace | The root graph has a workspace-specific incompatible edge | Run npm commands from the root with -w |
Common Causes
| Cause | Why npm rejects it | Durable fix |
|---|---|---|
| Framework major upgrade | Plugin peer range stops before the new major | Upgrade or replace the plugin |
| Old plugin with a narrow peer range | It declares support for only one host major | Use a maintained compatible release |
| Directly installed incompatible peers | Root dependencies request contradictory versions | Align both direct versions intentionally |
| A stale lockfile after manifest edits | package.json and package-lock.json describe different trees | Run a reviewed npm install and commit the lockfile |
| Root or workspace mismatch | Workspaces participate in one root resolution graph | Align versions at the root and affected workspace |
| Private registry metadata differs | The selected manifest or dist-tag may not match public npm | Inspect the configured registry and package manifest |
Unreviewed overrides | A pin forces a transitive version outside a package's tested range | Remove or update the override after verifying impact |
| Different local and CI npm config | Flags change how npm constructs the tree | Commit project-level config when an exception is approved |
Root Cause: One Graph Must Satisfy Every Range
A normal dependency says “this package needs a copy of another package.” A peer dependency says “this package must run beside the application's copy of a host package that matches this range.” Plugins use peer dependencies for hosts such as React, ESLint, webpack, TypeScript, Angular, and Vue so they do not quietly load a second incompatible host at runtime.
Since npm 7, peer dependencies are installed by default and conflicts can stop resolution. That stricter behavior is useful: two copies of a framework can look installed correctly while breaking context, hooks, plugins, type loading, or shared singleton state at runtime.
Read the error as a three-part sentence
Here is an illustrative report:
npm ERR! While resolving: storefront@1.0.0
npm ERR! Found: react@19.1.0
npm ERR! node_modules/react
npm ERR! react@"19.1.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^18.0.0" from legacy-react-widget@4.2.0
npm ERR! node_modules/legacy-react-widget
npm ERR! legacy-react-widget@"4.2.0" from the root projectRead it in this order:
- The root project wants
react@19.1.0. legacy-react-widget@4.2.0requires React^18.0.0as a peer.- No version can be both exactly 19.1.0 and within the 18.x range.
The error is not saying React is missing. It is saying npm cannot put the widget next to the React version you chose without violating the widget's declared compatibility contract.
Semver details that change the answer
| Range | Accepts | Does not accept |
| ---------- | --------------------------------------- | ---------------- | ----------------------- | ------ |
| 18.3.1 | only 18.3.1 | 18.3.2 or 19.0.0 |
| ^18.3.1 | 18.3.1 up to, but not including, 19.0.0 | 19.0.0 |
| ~18.3.1 | 18.3.1 up to, but not including, 18.4.0 | 18.4.0 |
| >=18 <20 | 18.x and 19.x | 20.0.0 |
| ^18 | | ^19 | compatible 18.x or 19.x | 20.0.0 |
Do not widen a peer range merely to silence npm. If you publish a package, use the broadest range that you have actually tested. If you consume a package, trust a narrow range until you have evidence from its release notes, tests, or maintainer that a later host major is supported.
Step-by-Step Solution
Step 1: Preserve evidence and locate the first real conflict
Do not delete node_modules or package-lock.json first. Capture the report
and inspect the project state:
npm --version
node --version
npm config get legacy-peer-deps
npm config get registry
git diff -- package.json package-lock.jsonThe first Could not resolve dependency block usually contains the primary
conflict. Later blocks can be consequences. If npm writes an
eresolve-report.txt path, keep it with the build log or pull request while
you diagnose the issue; it records the graph npm saw at that point.
Step 2: Inspect the package manifests and installed tree
Ask the registry what the candidate package declares before you change versions:
npm view react-dom@18.2.0 peerDependencies --json
npm view react-dom versions --json
npm view react-dom@latest peerDependencies --jsonThese commands use a real public package to demonstrate the syntax. For an
actual conflict, inspect the exact package and release named in your report. The
first command reads a pinned release, the second helps identify a newer release,
and the third shows whether the current dist-tag supports your host version.
For a private registry, run the same npm view query against the registry that
owns the scope; do not compare public npm metadata by accident.
After a previous install has produced a usable tree, these commands explain why a package exists and where duplicates came from:
npm explain react
npm ls react --allnpm explain prints the dependency chain that caused an installed package.
npm ls is diagnostic and can exit nonzero when the tree is invalid; that
nonzero status is useful evidence, not a reason to ignore its output.
Step 3: Choose one compatible version line
Use this order of preference:
- Upgrade the plugin or adapter. Find a release whose peer range includes your chosen host major. This is the usual fix after a React, Angular, TypeScript, ESLint, or webpack upgrade.
- Align the host version. If the plugin is required and a compatible release does not exist, use the supported host major while you plan the broader upgrade.
- Replace or remove the incompatible package. This is safer than freezing a central framework forever for an abandoned plugin.
- Contribute or wait for an upstream compatibility release. A peer range update should be accompanied by real tests against the new host version.
For the illustrative conflict, an aligned manifest might look like this:
{
"name": "storefront",
"private": true,
"dependencies": {
"react": "19.1.0",
"react-dom": "19.1.0",
"modern-react-widget": "6.0.0"
}
}The important property is not the package name. It is that the widget's
published peerDependencies.react range includes React 19. Do not install
React 18 beside React 19 just to make npm finish; applications that rely on a
single framework runtime are especially vulnerable to duplicate copies.
Step 4: Make the manifest change, then regenerate the lockfile
Edit package.json intentionally, then let npm resolve a new lockfile:
npm install
git diff -- package.json package-lock.jsonReview both files. The lockfile should change because the dependency policy changed. A giant unrelated lockfile rewrite usually means the command, npm version, registry, or existing manifest state changed too; investigate before committing it.
If you need to prove that the updated lockfile works from a clean dependency directory, use the command designed for that purpose:
rm -rf node_modules
npm ciOn Windows PowerShell:
Remove-Item -Recurse -Force node_modules
npm cinpm ci requires package-lock.json to agree with package.json and does not
rewrite either file. It is a verification and deployment command, not the tool
for choosing new versions.
macOS, Windows, Linux, WSL, CPU, and GPU
ERESOLVE is driven by package metadata and semver ranges, so its root cause
is platform-independent. Use the macOS/Linux rm -rf node_modules command or
the PowerShell alternative shown above only after the manifest and lockfile are
valid. Do not share a node_modules directory between Windows and WSL; run
npm ci in the environment that will execute the application.
CPU architecture, GPU availability, CUDA, and native module binaries can cause
later install or runtime failures, but they do not make incompatible peer ranges
compatible. In a container or cloud build, solve ERESOLVE first, then address
any platform-specific native dependency error separately.
When Flags and Overrides Are Appropriate
--legacy-peer-deps: a temporary exception, not a resolution
This command asks npm to ignore peer dependencies while constructing the tree:
npm install --legacy-peer-depsIt is useful for a narrow purpose: confirming that a package is the only resolution blocker, or unblocking a short-lived branch when you have separately validated runtime compatibility. It is not proof that the peer mismatch is safe.
If a team deliberately accepts this temporary risk, make the setting explicit and keep local and CI behavior identical:
# .npmrc
legacy-peer-deps=trueThen regenerate the lockfile with the same policy and use the same policy in CI:
npm install
npm ciRemove the setting once the package versions are aligned. npm documents that
legacy-peer-deps ignores peer dependencies, and recommends against it because
it no longer enforces contracts that meta-dependencies may rely on. Do not put
this setting in a global user .npmrc: it would silently affect unrelated
projects.
Why --force is usually worse
npm install --force tells npm to relax protections. It can turn a clear
install-time incompatibility into a later test or production failure. It is not
a substitute for checking package release notes, peer ranges, or runtime tests.
Avoid both commands when the conflict concerns:
- a framework singleton such as React, Angular, Vue, or a state-management host;
- build tooling that shares a compiler, loader, or plugin API;
- authentication, payment, cryptography, or security middleware;
- a production dependency that has not been tested with the forced tree.
overrides: enforce a transitive version, not peer compatibility
Root-level overrides are valuable when you need one reviewed version of a
transitive dependency, such as a patched parser or HTTP client. They do not
change the API compatibility claimed by a package's peerDependencies.
{
"name": "storefront",
"private": true,
"dependencies": {
"@company/sdk": "4.2.0"
},
"overrides": {
"@company/sdk": {
"undici": "6.21.1"
}
}
}This example asks npm to use undici@6.21.1 below @company/sdk. Review the
SDK's changelog and tests before committing the policy. An override can reduce
duplicate transitive versions or apply a vetted security fix; it cannot make a
React-18-only plugin actually support React 19.
For a direct dependency, npm rejects an override that specifies a different
version from the direct dependency itself. The npm documentation supports the
$name reference when the intended override must match a root dependency. That
restriction prevents a manifest from expressing contradictory top-level policy.
npm dedupe: cleanup after compatibility, not before it
npm dedupe can hoist compatible duplicate dependencies without adding new
modules. It is useful after an aligned install when you want a smaller tree:
npm dedupe
npm ls react --allIt cannot create an intersection between ^18 and ^19. Run it only after the
peer conflict is solved, and review the resulting lockfile as you would any
dependency change.
Workspaces, Docker, CI/CD, and Private Registries
npm workspaces
Workspaces share a root install and lockfile. A package inside one workspace can therefore constrain the same host package used by another workspace. Start from the repository root and scope the command only after you understand the root graph:
npm install -w @company/web
npm explain react -w @company/web
npm ls react --all --workspacesThe -w option selects a workspace, but it does not make an incompatible
monorepo peer relationship disappear. Put common framework versions in one
reviewed place, such as a root policy or shared package manifest, rather than
letting every workspace choose unrelated majors.
Docker and Linux containers
Docker is a clean-room test for the lockfile, not a special npm solver. Copy the manifests first, install deterministically, then copy application source:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["npm", "run", "start"]Do not copy the host node_modules directory into a Linux container. Native
modules, optional dependencies, CPU architecture, and operating system
differences can make a host installation invalid even when dependency versions
match. If a committed, non-secret project .npmrc defines a temporary
legacy-peer-deps policy, copy it before RUN npm ci. Do not bake registry
auth tokens into an image; inject them through the build platform's secret
mechanism instead.
CI/CD and cloud builds
Record the Node and npm versions in CI logs, use npm ci, and fail on an
unexpected lockfile change:
node --version
npm --version
npm ci
npm test
npm run buildIf a lockfile was created using options that affect the tree, such as
legacy-peer-deps or install-links, npm requires compatible settings for
npm ci. A local install that uses one set of flags and a cloud build that uses
another is not reproducible.
Private registries and scoped packages
An ERESOLVE report can be correct even if public npm shows a different peer
range. Your scope may resolve from a private registry, proxy, or cached package
metadata. Confirm the source before selecting a version:
npm config get registry
npm config list --location=projectThe output identifies the registry and project-level configuration that the
install uses. Then run npm view for the exact scoped package and registry
from that configuration. Never paste registry auth tokens, .npmrc files, or
complete npm debug logs into public issue trackers. Those logs can include
private package names, registry URLs, and credentials.
Incorrect Fixes and Why They Fail
| Incorrect approach | Why it is weak | Better action |
|---|---|---|
Delete package-lock.json immediately | Removes a reproducibility record but not incompatible ranges | Diagnose, align versions, then regenerate intentionally |
Delete node_modules repeatedly | Reinstalls the same impossible graph | Change the conflicting manifest constraints |
npm cache clean --force | Cache contents do not alter peer ranges | Use only for a proven cache problem |
| Add the peer package manually | It may strengthen the conflict or install another incompatible version | Match the plugin's declared peer range |
Move a package to devDependencies | npm still resolves peer relationships during install | Use a compatible release or remove it |
Commit global legacy-peer-deps | Hides conflicts in every project on a machine | Use a reviewed project-level temporary exception |
| Add an override for a peer mismatch | Pins a dependency but does not make APIs compatible | Upgrade, downgrade, replace, or test upstream support |
Blindly use --force in CI | Turns early, actionable failure into runtime risk | Make the graph and lockfile deterministic |
Verification Steps
An npm install that exits successfully is not enough. Verify the repaired
dependency graph at the boundaries where it will run:
-
Inspect the selected host version and dependency path.
npm ls react --all npm explain reactSubstitute the actual host package from your ERESOLVE report. Confirm that the expected version is present and that no unexpected duplicate copies were introduced.
-
Test the clean lockfile.
rm -rf node_modules npm cinpm cimust finish without modifyingpackage-lock.json. -
Run the checks that exercise the shared host.
npm test npm run buildFor a framework plugin, run at least the integration path that loads the plugin. An installable tree is not evidence that a peer API is compatible.
-
Repeat the same command in CI or Docker. Verify the Node version, npm version, registry, and project
.npmrcare the intended ones. -
Review the dependency diff. Confirm that package additions, removals, overrides, and lockfile changes are all explained by the chosen fix.
Prevention
Use these practices to avoid recurring ERESOLVE incidents:
- Upgrade a framework and its official adapters as one planned change, not as scattered individual package updates.
- Read peer dependencies before adding plugins to a major-version migration.
- Keep direct host packages aligned: React with React DOM, Angular packages, ESLint with its plugins, and compatible TypeScript tooling.
- Commit exactly one
package-lock.jsonfor an application and usenpm ciin CI, Docker, and release builds. - Pin or document Node and npm versions so local resolution matches CI.
- Keep project-specific npm configuration in a reviewed repository
.npmrc, never only in a developer's global settings. - Review every
overridesentry with an owner, reason, test evidence, and removal condition. - Run
npm outdatedon a schedule so package compatibility work is incremental rather than a multi-major emergency. - Run
npm auditafter the graph resolves, but do not let an automated audit upgrade bypass your peer compatibility review.
For related project debugging, see:
- fixing a missing dotenv module
- resolving a Node.js port collision
- debugging a JSON serialization boundary
- React 19 upgrade and compatibility notes
- common JavaScript coding patterns
- handling React request pressure
Official References
- npm
package.json: peer dependencies and overrides - npm configuration:
legacy-peer-deps - npm
explaincommand - npm
lscommand - npm
cicommand - npm
package-lock.jsondocumentation - npm
dedupecommand
FAQs
How do I fix npm ERR! ERESOLVE unable to resolve dependency tree?
Read the Found version and the peer range in the first conflict block. Then
install a plugin version that supports the chosen host, align the host version
to the plugin's supported major, or replace the plugin. Run npm install to
update the lockfile and npm ci from a clean directory to verify the committed
tree.
Is npm install --legacy-peer-deps a safe permanent fix?
No. It makes npm ignore peer dependencies during resolution, so it stops npm from enforcing a contract that may be required at runtime. Use it only as a documented, temporary exception after testing, and remove it when compatible versions are available. Do not add it globally for every npm project.
Why does npm ci fail when npm install succeeds?
npm install is allowed to resolve and update package-lock.json. npm ci
requires the lockfile to agree with package.json and installs that exact tree
without rewriting it. It also needs compatible npm configuration. Recreate the
lockfile deliberately with the project settings, commit it, and run the same
settings in CI.
Can npm overrides solve a peer dependency conflict?
Usually not. Overrides decide which version of an existing dependency edge npm installs. They do not change the peer range declared by a plugin or guarantee that a plugin works with a new framework API. Use overrides for a reviewed transitive pin, not to claim unsupported framework compatibility.
Does removing node_modules or clearing the npm cache fix ERESOLVE?
Not when semver ranges conflict. A fresh install reads the same package.json
and package manifests, so it reaches the same contradiction. Removing
node_modules is useful after you have changed and committed a valid lockfile;
cache cleaning is only relevant when you have evidence of a cache issue.
Why do React peer dependency conflicts matter so much?
React packages often need to share one compatible runtime. Installing mismatched or duplicate copies can result in invalid hook calls, lost context, renderer mismatches, or libraries that appear installed but fail when rendered. Treat a React peer conflict as a compatibility decision, not merely an npm warning.
Key takeaways
- •ERESOLVE is a version-constraint conflict, usually a peer dependency mismatch—not a corrupted node_modules folder.
- •The npm report identifies the installed or requested version, the package that requires a range, and the incompatible range.
- •Update the plugin, align the host framework version, or replace the unsupported package before reaching for install flags.
- •legacy-peer-deps ignores peer contracts; use it only as a documented, temporary compatibility exception.
- •npm overrides can pin a transitive dependency but cannot make an incompatible peer API work.
- •Commit one reviewed package-lock.json and make local, Docker, and CI installs use compatible npm configuration.
Frequently asked questions
How do I fix npm ERR! ERESOLVE unable to resolve dependency tree?
Read the ERESOLVE report to find the host package npm found and the peer range required by the conflicting package. Install a version of the plugin that supports the host, change the host to a supported version, or replace the incompatible plugin. Then run npm install to update package-lock.json and verify a clean checkout with npm ci. Avoid using legacy-peer-deps as the permanent fix.
What causes npm ERESOLVE?
ERESOLVE occurs when npm cannot build one dependency graph that satisfies every declared version range. The most common cause is an incompatible peer dependency: for example, an application requests React 19 while a plugin declares peerDependencies of React 18 only. It can also result from incompatible workspaces, a stale lockfile after manifest edits, private-registry metadata, or a conflicting override.
Is npm install --legacy-peer-deps safe?
It can unblock a local investigation, but it is not a generally safe fix. npm documents that legacy-peer-deps ignores peerDependencies while constructing the tree, so npm no longer verifies a compatibility contract that the packages may rely on. If a team accepts it temporarily, record the reason and use the same setting in npm install and npm ci until the dependency conflict is fixed.
Should I use npm install --force for ERESOLVE?
Usually no. Force tells npm to continue past protections, but it does not prove that mismatched packages work at runtime. It can create a tree that installs but fails later through duplicate framework singletons, missing APIs, invalid hooks, or plugin crashes. Prefer an aligned upgrade or downgrade, a maintained replacement, or an upstream fix.
Does deleting package-lock.json fix ERESOLVE?
Not by itself. ERESOLVE is usually caused by constraints in package.json and dependency manifests, which deleting a lockfile does not change. Removing the lock can also make npm choose newer transitive versions and hide the original evidence. Change the intended version constraints first, then regenerate and review the lockfile deliberately.
What is the difference between npm install and npm ci after an ERESOLVE fix?
npm install resolves the manifest and updates package-lock.json when needed. npm ci requires a lockfile that agrees with package.json, removes existing node_modules, and installs the locked tree without modifying the lockfile. Use npm install while deliberately repairing the graph; commit the reviewed lockfile; then use npm ci in CI, Docker, and clean verification.