--legacy-peer-deps restores npm 3-6 behaviour: peer dependencies are not installed automatically and conflicts are downgraded from errors to warnings. It gets you past a failed install immediately, and it does that by ignoring the thing npm was trying to tell you.
Almost everyone meets this flag the same way. An install fails with a wall of ERESOLVE unable to resolve dependency tree, the first search result says add --legacy-peer-deps, it works, and the moment passes.
That is often the correct call. It is also sometimes how you end up debugging a runtime error three weeks later that no one can reproduce. The difference is entirely about which kind of conflict you had, and you can usually tell in about two minutes.
Table of contents
- What changed in npm 7
- The two conflicts that look identical and are not
- —legacy-peer-deps versus —force
- Making it stick without typing it every time
- When to stop using the flag
- How this fits the rest of the stack
- FAQ
What changed in npm 7
A peer dependency is a package declaring what it expects to be installed alongside it. A React component library saying "peerDependencies": { "react": "^17.0.0" } is saying it was built against React 17 and expects the host application to supply it.
npm 3 through 6 printed a warning if that expectation was not met and installed anyway. npm 7 changed two things at once: peer dependencies are installed automatically, and an unsatisfiable peer requirement is a hard error.
So the flood of ERESOLVE failures in 2021 was not a wave of newly broken packages. Those conflicts had always existed. npm simply stopped hiding them.
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! Found: react@18.2.0
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.0" from some-legacy-lib@2.1.0
Read that block rather than skipping it. It names the package that has not kept up, and the version it wants. Those two facts decide everything that follows.
The two conflicts that look identical and are not
The stale declaration. The library works fine with React 18 — nobody has published a release widening the peer range. The peerDependencies field is out of date, not the code. This is extremely common on packages that are stable and lightly maintained.
The real incompatibility. The library genuinely uses an API that changed. It will install, and it will break at runtime, usually somewhere unhelpful and usually not immediately.
npm cannot tell these apart. It only sees a range that does not match. You can, in about two minutes:
- Open the library’s repository and look at the last few releases and the open issues. Someone has always already asked about the new major version.
- Check whether a newer version of the library exists that widens the range —
npm view some-legacy-lib versions --jsonshows what is published. - If the library was last touched three years ago and the major version bumped six months ago, assume incompatibility until proven otherwise.
For a stale declaration, --legacy-peer-deps is exactly right and costs you nothing. For a real incompatibility, it converts a clear install-time failure into an unclear runtime one.
—legacy-peer-deps versus —force
These get suggested interchangeably and they are not interchangeable.
--legacy-peer-deps ignores peer dependencies specifically. Everything else about resolution behaves normally, and you get warnings naming what it skipped.
--force is much broader. It overrides multiple safety checks, will install conflicting versions of the same package, and can produce a tree that no version of npm would have chosen on purpose.
If you have an ERESOLVE peer conflict, --legacy-peer-deps is the targeted tool. --force is a larger hammer aimed at a different problem, and reaching for it first is how you end up with two copies of React in one bundle and hooks failing for no visible reason.
There is also overrides, which is the precise option when you know exactly which version you want to win:
{
"overrides": {
"some-legacy-lib": {
"react": "$react"
}
}
}
That says: for this one package, use whatever React the root project uses. It is narrower than a global flag and it documents the decision in the file where the next person will look.
Making it stick without typing it every time
A flag that only some people pass is a flag that produces different node_modules on different machines. If the project needs it, put it in the project.
echo "legacy-peer-deps=true" >> .npmrc
Commit that file. Now local installs, teammates’ installs, and the build all resolve the same way, which is the entire point.
One caveat that catches people: npm ci reads .npmrc but installs strictly from package-lock.json. If the lockfile was generated with the flag and the CI environment does not have the .npmrc, you can get divergent behaviour. Committing the file avoids that whole class of problem.
Add a comment where the next person will find it — a line in the README saying which package needs the flag and why. A bare legacy-peer-deps=true with no explanation gets copied forward for years after the underlying package was fixed.
When to stop using the flag
Treat it as a marker, not a setting. It is recording that one dependency has fallen behind.
Periodically, remove it and see what happens:
rm -rf node_modules package-lock.json
npm install
# clean install means the upstream range was widened -- drop the flag
If that succeeds, delete the line from .npmrc and commit. If it fails, the output names the package still holding you back, and now you have a concrete decision: upgrade it, replace it, or keep the flag for another quarter.
The failure mode worth avoiding is the project where the flag has been on for three years, nobody remembers why, and the install now silently ignores four genuine conflicts because one package was stale in 2022. That is not a hypothetical — it is what happens when a workaround is treated as configuration.
How this fits the rest of the stack
Dependency resolution is one of those problems that only bites when the environment differs. It works locally, the lockfile is fine, and then the build produces a different tree because a flag was passed in one place and not the other.
The defence is making the build environment explicit and identical every time: the flag in a committed .npmrc, the lockfile in the repository, the Node version pinned. On RunxBuild the build runs from your repository with the environment you configured, and the build log shows exactly what was installed — so a resolution difference shows up in the log rather than as a mystery in production. If you want to see what the running service costs alongside the database and storage it needs, the RunxBuild hosting calculator breaks it out per line item.
Useful related references:
- “Connection Reset by Peer” Is the Loudest Error in Networking, and Here Is Where It Actually Comes From
- npm install a Specific Version: Pinning, Ranges, and the Lockfile
- npm Clean Install: When Deleting node_modules Is Right, When
npm ciIs Right, and When Both Are Slowing You Down - Services on RunxBuild
FAQ
Is —legacy-peer-deps safe to use in production?
It is safe when the peer conflict is a stale version range in a package that actually works with your version. It is not safe when the package genuinely depends on an API that changed, because you are converting an install-time failure into a runtime one. Spend two minutes on the library’s issue tracker before deciding which you have.
What is the difference between —legacy-peer-deps and —force?
--legacy-peer-deps ignores peer dependency checks and nothing else. --force overrides several safety mechanisms and can install conflicting duplicates of the same package. For an ERESOLVE peer error, use the first one. --force is a broader tool for a different problem.
Should I put legacy-peer-deps in .npmrc or pass it on the command line?
Put it in a committed .npmrc if the project genuinely needs it. A flag that only some people pass produces different dependency trees on different machines, and that difference will eventually be the cause of a bug that only reproduces in CI.
Does yarn or pnpm have this problem?
They handle it differently. Yarn 1 warns on peer mismatches and continues, which is close to the old npm behaviour. pnpm errors by default but lets you resolve it per package through peerDependencyRules in the workspace file, which is closer to npm’s overrides than to a blanket flag.
How do I find which package is causing the conflict?
Read the ERESOLVE block rather than scrolling past it. The Found: line shows what is installed and the peer ... from line names the package with the unsatisfied requirement and its version. Those two lines are the whole diagnosis.