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:

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.

Resolution & Config Patch

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

  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

CLI Validation & Debug Commands

Confirm the conflict is genuinely resolved rather than merely silenced:

# 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.

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.

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.

Related

Dependency Resolution Explained