Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

Fixing npm ERESOLVE Peer Dependency Conflicts

Exact Symptoms

npm install halts and prints an ERESOLVE block. The verbatim error text looks like this:

Exact Symptoms npm install halts and prints an ERESOLVE block. Exact Symptoms npm install halts and prints an ERESOLVE block.
Exact Symptoms — the core idea of this section at a glance.
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: my-app@1.0.0
npm error Found: react@17.0.2
npm error node_modules/react
npm error   react@"17.0.2" from the root project
npm error
npm error Could not resolve dependency:
npm error peer react@"^18.0.0" from @acme/ui-kit@3.2.0
npm error node_modules/@acme/ui-kit
npm error   @acme/ui-kit@"^3.2.0" from the root project
npm error
npm error Conflicting peer dependency: react@18.3.1
npm error node_modules/react
npm error   peer react@"^18.0.0" from @acme/ui-kit@3.2.0

The two diagnostic lines that always appear are Could not resolve dependency: and a peer <pkg>@"<range>" from <pkg>@<version> block, frequently followed by Conflicting peer dependency:.

Root Cause Analysis

ERESOLVE is npm's signal that it cannot build a single, internally consistent dependency tree. Every package declares peerDependencies — version ranges it expects the surrounding tree to satisfy — and since npm v7 the installer treats those declarations as strict, automatically installed contracts rather than as warnings (the pre-v7 behavior). When two branches of your tree demand mutually exclusive versions of the same peer (one package wants react@^17, another wants react@^18), there is no version that satisfies both, so npm aborts before writing anything.

The conflict is structural, not cosmetic: it reflects a genuine disagreement about what version of a shared dependency should exist in node_modules. Resolving it correctly means making the tree consistent, not silencing the messenger. Because the failure is rooted in how the resolver walks declared ranges across the whole graph, understanding Dependency Resolution Explained is the foundation for every fix below. The same misclassification that triggers these errors is covered in depth in When to Use peerDependencies vs devDependencies.

ERESOLVE decision flow A decision tree from an ERESOLVE error toward version alignment, overrides, or legacy-peer-deps as a last resort. ERESOLVE error npm install aborts Can you align versions? npm ls / npm explain Bump / align dep preferred fix overrides field force one version --legacy-peer-deps last resort, masks the real conflict yes no stuck Prefer the left path. Only reach right when a transitive peer cannot be changed.
Decide from left to right: align versions first, override second, and treat flags as the exception.

It helps to distinguish the two shapes a peer conflict takes, because they call for different fixes. In a direct conflict, a package you installed declares a peer range that the framework version in your tree does not satisfy — a plugin requiring the previous major while your application is on the current one. In a transitive conflict, the incompatible peer is requested by a dependency of a dependency, so you did not choose it directly and cannot simply change a range in your own manifest. Reading the ERESOLVE output, which prints the full chain of who requires what, tells you which shape you are dealing with and therefore which lever to reach for.

The reason npm surfaces this as a hard error rather than a warning is that an unsatisfied peer is a genuine correctness risk, not a style preference. A plugin loaded against a framework version it was not written for can call APIs that no longer exist, rely on behavior that changed, or — for a framework whose identity matters — end up as a second copy that breaks context and hooks. The error is npm declining to build a tree it can prove violates a stated requirement, which is a service: it converts a latent runtime failure into an install-time signal you can act on immediately.

Resolution & Config Patch

Work the conflict in the order below. Each step is strictly preferable to the one after it.

Resolution & Config Patch Work the conflict in the order below. Resolution & Config Patch Work the conflict in the order below.
Resolution & Config Patch — the core idea of this section at a glance.
  1. Read the conflict, don't guess. The ERESOLVE block names both sides: the version Found: in the tree and the peer ... from ... package that disagrees. Identify which package's peer range is unsatisfiable and what version would satisfy everyone.

  2. Align versions at the source (preferred). If your root project pins react@17 but @acme/ui-kit requires react@^18, bump the shared dependency so both ranges overlap:

    npm install react@^18 react-dom@^18

    If instead an outdated package declares the narrow peer, upgrade that package to a release whose peer range includes the version you already use:

    npm install @acme/ui-kit@latest

    Version alignment is the only fix that produces a tree npm itself considers valid. Always try it first.

  3. Pin a single version with overrides (when you cannot upgrade). When a transitive dependency declares an over-strict or stale peer range that the maintainer has not fixed, force the whole tree onto one version using the overrides field in package.json:

    {
      "overrides": {
        "react": "$react",
        "react-dom": "$react-dom"
      }
    }

    The $react syntax reuses the version you declared in your own dependencies, keeping a single source of truth. You can also pin literally:

    {
      "overrides": {
        "@acme/ui-kit": {
          "react": "18.3.1"
        }
      }
    }

    The nested form scopes the override so only @acme/ui-kit sees the forced react, which is safer than a global override. This is the right tool when the conflicting peer lives deep in the tree and collapsing it to one copy is also how you fix Deduplicating Duplicate React Versions.

  4. --legacy-peer-deps (last resort). This flag restores npm v6 behavior: peer dependencies are no longer auto-installed or enforced, so npm ignores the conflict and installs anyway:

    npm install --legacy-peer-deps

    It is a last resort because it does not fix anything — it disables the check that caught a real incompatibility. You may end up running a package against a peer version it was never tested with, surfacing as runtime crashes rather than install errors. If you must use it, scope it narrowly and document why.

  5. --force (almost never). npm install --force is broader still: it overrides multiple safety checks, not just peers, and will happily install a tree npm believes is broken. Reach for it only to reproduce a problem or in a throwaway environment, never in CI or a committed setup.

After whichever step applies, regenerate the lockfile and reinstall cleanly:

rm -rf node_modules package-lock.json
npm install

When an override is the right lever, scope it as narrowly as the conflict requires and treat it as temporary. A global override that forces a framework version everywhere is a broad claim about compatibility; a scoped one that pins the version only under the conflicting dependency limits the blast radius to exactly the subtree that needs it. Either way, document why the override exists and track the upstream change that will let you remove it, so your manifest reflects deliberate, current decisions rather than a growing pile of pins whose reasons have been forgotten. After applying any override, re-run the install and inspect the graph to confirm the conflict is genuinely resolved rather than merely silenced.

CLI Validation & Debug Commands

Confirm the conflict is genuinely resolved rather than merely silenced:

CLI Validation & Debug Commands Confirm the conflict is genuinely resolved rather than merely silenced: CLI Validation & Debug Commands Confirm the conflict is genuinely resolved rather than merely silenced:
CLI Validation & Debug Commands — the core idea of this section at a glance.
# How many copies of the package exist, and where?
npm ls react

# Why is THIS version in the tree? Traces the requiring chain.
npm explain react

# Reproduce a clean, deterministic install (fails on any conflict)
rm -rf node_modules && npm ci

npm ls react should report a single version with no invalid or UNMET PEER DEPENDENCY annotations. npm explain react prints the full chain of packages that pulled the version in, which tells you exactly which dependency to upgrade or override. A successful npm ci against the committed lockfile is the strongest proof: it reinstalls from scratch and re-runs peer resolution, so a passing npm ci means the conflict is structurally gone.

The most useful debugging command is the one that shows you the conflict before you try to fix it. Running the install with increased verbosity prints the full requirement chain, and inspecting the resolved graph afterward confirms whether the fix worked. Reading the actual versions and ranges — rather than guessing — is what turns a peer conflict from a frustrating wall into a solvable puzzle: the error already names the incompatible requirements, and the graph tools show you exactly how the tree resolved, so the reconciliation is a matter of acting on information the tooling gives you rather than trial and error.

Prevention & CI Guardrails

  • Fail fast in CI. Run npm ci (not npm install) so the pipeline reinstalls from the lockfile and surfaces ERESOLVE before merge, with no implicit --force.
  • Ban silent bypasses. Forbid --legacy-peer-deps and --force in CI scripts; if they exist, require a code comment and an issue link justifying each one.
  • Keep overrides auditable. Review every entry in overrides during PR review — each one is a manual override of the resolver and should expire once upstream ships a fix.
  • Pin the package manager. Add "packageManager": "npm@10.x" so developers and CI resolve peers identically and you don't chase version-specific ERESOLVE differences.
  • Catch misclassification early. Lint package.json so runtime host libraries land in peerDependencies and tooling stays in devDependencies, the root cause of most avoidable conflicts.
Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows. Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows.
Prevention & CI Guardrails — the core idea of this section at a glance.

The most effective prevention is keeping your own peer ranges honest and your framework versions aligned. A library you maintain should declare peer ranges as wide as it genuinely supports, so consumers on any compatible version resolve cleanly rather than hitting a spurious conflict; an over-narrow peer range is a common self-inflicted cause of ERESOLVE for your consumers. On the consuming side, keeping shared frameworks on a single, current version across the workspace means fewer packages request incompatible ranges in the first place.

Treat a suppressed conflict as technical debt to resolve, not a permanent state. If you must ship with --legacy-peer-deps to unblock an install, record why and track the upstream fix — the package with the over-strict peer range, or the provider that needs updating — so the suppression is removed once the ecosystem catches up. Running the install without the suppression flag in a scheduled CI job surfaces when the conflict has genuinely resolved upstream, so you can drop the workaround rather than carrying it indefinitely. A dependency graph that resolves cleanly without suppression flags is both more trustworthy and easier to update than one whose conflicts are permanently silenced.

Why ERESOLVE happens and what it protects

ERESOLVE fires when npm cannot find a single set of versions that satisfies every peer-dependency requirement in the tree simultaneously. A package declares a peer range — say a plugin requires a framework >=17 <18 — and another part of the tree provides a version outside that range, so no installation satisfies both constraints. npm refuses rather than install a tree it knows violates a declared requirement, which is the error protecting you from a runtime failure the peer declaration was warning about.

Unsatisfiable constraint No version set satisfies every peer requirement. peer range plugin needs >=17 <18 provider outside framework 18 installed no valid set npm refuses
ERESOLVE means a genuine version conflict — a peer range and the tree disagree.

The error is stricter in npm 7+ than the older behavior precisely because unsatisfied peers cause real bugs — a plugin using a framework version it does not support, or two incompatible copies of a shared dependency. So ERESOLVE is a signal that a genuine version conflict exists in your dependency graph, not merely npm being fussy. The right response is to reconcile the conflict: update the package with the narrow peer range, update the provider to a version the peer accepts, or confirm the peer range is unnecessarily strict and should be widened upstream. Understanding that the error reflects an actual unsatisfiable constraint — not a tooling quirk — is what steers you toward fixing the conflict rather than suppressing the check.

Resolving without --force or --legacy-peer-deps

The tempting shortcuts — --force and --legacy-peer-deps — suppress the check rather than resolve the conflict, and they can leave you with a genuinely incompatible tree that fails at runtime instead of at install. --legacy-peer-deps restores npm's old behavior of ignoring peer conflicts, and --force overrides them outright; both trade a loud, actionable install error for a silent, latent one. Reserve them for a known-safe case where you have verified the conflict is harmless, not as a default.

Reconcile or suppress How to resolve a peer conflict properly. What's the conflict? narrow peer range update or widen provider version align it transitive peer override
Reconcile the actual conflict rather than suppressing the check.

The durable fixes reconcile the actual conflict. Read the error, which names the conflicting requirements, and choose the real fix: upgrade the dependency whose peer range is too narrow to a version that accepts the installed framework; downgrade or upgrade the provider to a version the peer supports; or, if the peer range is genuinely over-strict, file an issue upstream (or patch it locally) to widen it. Where a transitive peer is the problem, an overrides entry can pin a compatible version. Each of these produces a tree that actually satisfies the constraints, so the install succeeds because the conflict is gone — not because the check was bypassed. Choosing reconciliation over suppression is the difference between a resolved dependency graph and a hidden incompatibility waiting to surface as a broken hook dispatcher or a doubled framework at runtime.

How each package manager handles peer conflicts

The three package managers treat peer conflicts differently, and knowing the differences explains why the same dependency tree installs cleanly under one and errors under another. npm 7+ enforces peers strictly by default, refusing an install that violates a peer range — which is what produces ERESOLVE. pnpm also surfaces peer issues but is often more precise about which package is responsible, and its strict, symlinked layout makes a doubled framework fail loudly rather than silently. Yarn's behavior depends on the version, with Berry generally warning rather than hard-failing on peers.

Peer handling How npm, pnpm, and Yarn treat peer conflicts. Manager Peer conflict Note npm 7+ hard error ERESOLVE pnpm surfaces precisely strict layout Yarn Berry often warns version-dependent
The conflict is the same everywhere; only how loudly the tool complains differs.

This variation matters when a project moves between managers or when a contributor's local install behaves differently from CI. A tree that npm rejects with ERESOLVE might install with a warning under a different manager — but the underlying conflict is the same, and the warning is not a resolution. The right response regardless of manager is to reconcile the actual version conflict rather than rely on a particular tool's leniency, because a peer that is unsatisfied under npm's strict resolution is unsatisfied everywhere; only the loudness of the complaint differs. Pinning the package manager with packageManager and Corepack ensures everyone sees the same behavior, so a conflict surfaces uniformly rather than appearing only for the developers on the strictest tool.

A worked example: resolving a real conflict

Walking through a concrete conflict makes the reconciliation approach concrete. Suppose npm install fails with an ERESOLVE naming a charting library that declares a peer of react@^17 while your application is on react@18. The error prints the chain: your app depends on react@18, the charting library depends on react@^17, and the two cannot coexist. The question is which of the three levers applies.

Worked example Check for a newer version, verify, or accept. read the chain who needs what newer version? upgrade if so verify or accept override or wait
Read the named requirements, check for a compatible version, and choose the matching fix.

The first check is whether the charting library has a newer version that supports React 18 — often it does, and upgrading it (npm install charting-lib@latest) resolves the conflict because the newer version widens its peer range. If no such version exists, the peer range may be over-cautious, in which case checking the library's issue tracker or source reveals whether React 18 actually works; if it does, a temporary overrides entry forcing the library to accept React 18, or --legacy-peer-deps with a tracked follow-up, unblocks you while you wait for an official release. If the library genuinely does not support React 18, the honest conclusion is that you cannot use that version yet, and the conflict is telling you a real incompatibility rather than a resolvable one.

The discipline the example illustrates is to read the named requirements, check whether a compatible version exists, and choose the fix that matches — upgrade, override with verification, or accept the incompatibility. This is the opposite of reflexively adding --legacy-peer-deps, which would have installed a tree with React 17 expectations against React 18, potentially breaking the charting library at runtime in ways far harder to diagnose than the install error that warned you.

Frequently Asked Questions

What is the difference between --legacy-peer-deps and --force? --legacy-peer-deps only disables automatic installation and enforcement of peer dependencies, mimicking npm v6. --force is much broader — it overrides peer checks plus other integrity and resolution guards, and will install a tree npm considers invalid. Prefer --legacy-peer-deps if you must bypass at all, and prefer overrides over both.

Will overrides break anything? It can. Forcing a single version means a package that requested a different one now runs against code it may not have been tested with. Scope overrides to the specific dependency that needs them (the nested form), pin to a version inside every consumer's expected range where possible, and remove the override once the upstream peer range is fixed.

Why did this work in npm v6 but break after upgrading? npm v7+ installs and strictly enforces peerDependencies automatically; npm v6 only warned about unmet peers and never installed them. An upgrade can therefore surface a latent conflict that was always present but previously ignored. The fix is to align versions, not to permanently fall back to --legacy-peer-deps.

How do I find which package is causing the conflict? Run npm explain <package> for the conflicted dependency. It prints every chain that requires it, so you can see which package declares the incompatible peer range and decide whether to upgrade that package or scope an override to it.

What does an ERESOLVE error actually mean?

npm cannot find a single set of versions that satisfies every peer-dependency requirement at once — a genuine version conflict where a peer range and the installed version disagree. It is protecting you from a tree that violates a declared requirement, not being fussy.

Should I use --force or --legacy-peer-deps to fix ERESOLVE?

Only for a case you have verified is harmless. Both suppress the check rather than resolve the conflict, trading a loud install error for a silent runtime one. Prefer reconciling the actual conflict — update the package with the narrow peer, update the provider, or widen an over-strict range upstream.

How do I fix a peer conflict in a transitive dependency?

Pin a compatible version with an overrides entry, so the transitive peer resolves to a version that satisfies the constraint. Verify with npm ls that the graph now resolves cleanly, rather than suppressing the check with --legacy-peer-deps.

Why does the same install fail under npm but warn under Yarn?

npm 7+ enforces peer dependencies strictly and refuses a violating tree with ERESOLVE, while some Yarn and pnpm configurations warn instead. The underlying conflict is identical — an unsatisfied peer is unsatisfied everywhere; only the loudness differs. Reconcile the actual conflict rather than relying on a tool's leniency.

A library's peer requires an older framework major than I'm on — what do I do?

First check for a newer version of the library that supports your major; upgrading it usually widens the peer range and resolves the conflict. If none exists but the library actually works with your version, use a verified overrides pin with a tracked follow-up. If it genuinely doesn't support your version, the conflict is a real incompatibility, not something to suppress.

Related

Dependency Resolution Explained