Handling peer dependency conflicts in practice
Peer dependency errors can stop an install cold. Understanding the error tree, the version negotiation involved, and the tradeoffs of each workaround turns a cryptic failure into a solvable constraint.
If you have worked on a JavaScript project with more than a handful of dependencies, you have probably seen the wall of red that starts with ERESOLVE unable to resolve dependency tree. It is easy to reach for --legacy-peer-deps or --force and move on. That works—until it does not. This guide walks through what peer dependencies are, how they changed across npm versions, and a repeatable process for diagnosing and resolving the conflict properly.
1. What peer dependencies express
A peerDependency is a compatibility contract. When package A declares a peer dependency on package B at version 2.x, A is saying "I need B installed alongside me at version 2, but I do not want to ship my own copy." This pattern is common for plugins: a lint rule for ESLint, a build tool for Webpack, or a UI library for React. The host owns the single copy, and every plugin must agree on which version that is.
In npm 3 through 6, peer dependencies were not installed automatically. If a peer was missing or mismatched, npm issued a warning and continued. The tree was often broken at runtime, but the install command succeeded. npm 7 changed this: peers are now installed automatically by default, and if two packages disagree on the peer version, the install fails with ERESOLVE. This was a deliberate shift to surface conflicts at install time rather than at runtime.
2. Read the error tree before changing anything
The ERESOLVE error prints a tree showing exactly which packages conflict. Before running any command, read that tree top to bottom. It tells you three things: which package triggered the resolution, which peer dependency is in dispute, and which version each parent expects.
npm explainRun npm explain <package> to see why every instance of a package appears in the tree. This is often faster than reading the full error and reveals transitive peer chains that the error message truncates.For example, if the error says eslint-plugin-foo requires eslint@^7 but eslint-plugin-bar requires eslint@^8, the tree has two incompatible constraints on the same host. Write down the exact versions and ranges before you touch a config file.
3. Align versions at the source
The cleanest fix is to satisfy both constraints with a single version. Check whether a newer release of one plugin supports the same host version as the other. Visit the registry with npm view eslint-plugin-foo peerDependencies to see what host ranges each published version declares. You are looking for a version of the plugin that widens its peer range or moves to the newer host line.
Wide peer ranges are a best practice for plugin authors. A plugin that works with every ^7 release should declare "eslint": "^7.0.0", not "eslint": "7.32.0". When you control the plugin, prefer ranges over exact pins. When you consume the plugin, lobby upstream for broader ranges.
Another option is upgrading the host. If you are still on an older major version of React, Webpack, or ESLint, the blockers are often plugins that have already moved on. A host upgrade unlocks the latest plugin versions and resolves the peer conflict at the same time. Treat this as a migration project, not a quick fix.
4. Use peerDependenciesMeta for optional peers
Some peer dependencies are truly optional. A plugin may integrate with multiple backends but only need one. The package author can mark peers as optional via peerDependenciesMeta:
When a peer is optional, npm does not require it to be installed. As a consumer, you cannot add peerDependenciesMeta to someone else's package, but you can use this information to decide whether an optional peer is worth installing at all. If the plugin works without it, leave it out.
5. Overrides and resolutions only when justified
npm's overrides and Yarn's resolutions let you force a specific transitive package version anywhere in the tree. They do not make an incompatible peer contract compatible; they change what gets resolved. Use them only when you have verified that the resulting combination is compatible at runtime.
A safer override pattern is to pin a vulnerable or duplicated transitive dependency after confirming that the direct parent supports the patched release. If the conflict is between a plugin's peer range and your host, prefer upgrading the plugin or changing the host version instead of overriding the peer itself.
A dangerous override is forcing a transitive version that the parent has not tested against. Always run the full test suite after adding an override. If the parent calls an API that changed in the newer release, the error moves from install time to runtime and may be harder to debug.
Yarn Berry added packageExtensions as a third option, which lets you mutate the peer dependency range of any package in the tree without overriding its version. This can resolve a false conflict when a plugin's declared range is narrower than its actual compatibility. Check the Yarn documentation for the exact syntax.
6. Document temporary escape hatches
Sometimes you need a fix now and a proper fix later. npm offers two flags that bypass peer conflict errors:
--legacy-peer-deps reverts to npm 6 behavior: npm warns about peer mismatches but installs the tree anyway. This is useful during a hotfix or when an upstream package has not released a compatibility update yet.
--force tells npm to proceed even when the tree is invalid. It is a more aggressive flag that can produce a broken node_modules. Reserve it for CI experiments or local debugging where you can clean up immediately.
If you use either flag in a production project, document it. Add a comment in your README or a pinned issue that says which flag was needed, which versions were in conflict, and what the exit criteria are for removing the flag. Otherwise it becomes permanent debt that nobody remembers how to resolve.
7. Monorepo considerations
Monorepos amplify peer conflicts because a single host version (e.g., React, Next.js, TypeScript) must satisfy every package in the workspace. Tools like pnpm enforce strict peer resolution by default and do not offer a --legacy-peer-deps equivalent. The only reliable strategy is to keep the host version consistent across all workspace packages and use pnpm.overrides or a shared.npmrc with strict-peer-dependencies=false as a last resort. Document that config change the same way you would document--legacy-peer-deps.
8. Prevention is cheaper than diagnosis
The best way to handle peer conflicts is to avoid introducing them. Review peer dependency ranges when you add a new plugin. Prefer plugins that declare wide ranges (e.g., ^16.8.0 || ^17.0.0 || ^18.0.0). Pin the host explicitly in your own dependencies so the version is unambiguous. Run npm outdated regularly to keep the host current so you are not stuck several major versions behind where the ecosystem has moved.
Finally, treat ERESOLVE as useful information. It is npm telling you that your dependency tree has a constraint that cannot be satisfied. The right fix is usually to change the constraint, not to silence the resolver.
Sources and further reading
- npm Docs: package.json (peerDependencies section) — Canonical reference for the peerDependencies and peerDependenciesMeta fields.
- npm Docs: legacy-peer-deps config — Description of the flag that reverts peer resolution to npm 6 behavior.
- npm Docs: overrides — How to force specific package versions anywhere in the dependency tree.
- GitHub Blog: npm 7 is now generally available — Announcement covering automatic peer dependency installation and the new ERESOLVE algorithm.
- npm CLI v7.0.0 release notes — Release notes covering automatic peer installation and the new resolver.
- semver.org — The semantic versioning specification that peer dependency ranges are built on.
- Yarn Berry: packageExtensions — Yarn's mechanism for extending a package's declared peer dependency ranges without overriding versions.
- pnpm: overrides — pnpm's equivalent of npm overrides for monorepo-friendly forced version resolution.