Fix: NPM Dependency Hell
Error Message:
"ERESOLVE unable to resolve dependency tree" "npm ERR! Conflicting peer dependency: react@18.2.0 vs react@19.0.0" "Module not found: Error: Can't resolve 'framer-motion'" "npm ERR! ERESOLVE could not resolve"
What This Error Means
npm refused to install your requested package because the dependency graph contains a contradiction — two packages require incompatible versions of the same dependency, or a peer dependency constraint can't be satisfied. The build then fails because the module that should exist either isn't installed or is the wrong version.
Root Cause Analysis
The modern npm dependency resolver (v7+) enforces peer dependency constraints strictly. In npm v6 and earlier, peer dependency conflicts produced a warning, not an error — installs succeeded and you dealt with runtime breakage later. npm v7+ treats them as hard failures, blocking the install entirely. This is safer in theory, but in practice it means a single outdated peer constraint in a transitive dependency can lock your entire install.
Claude makes this worse in a specific way: when you ask it to "add framer-motion," it runs npm install framer-motion without first checking what version of React your project uses. If framer-motion's latest release requires React 19 as a peer, and your project is on React 18, npm throws ERESOLVE. Claude might then try npm install framer-motion@latest --force, which bypasses the check but installs a package that won't actually work at runtime — you get a clean install and a broken app.
A second pattern is version drift across lockfiles. If Claude edits package.json to bump a version but doesn't regenerate package-lock.json, the two files disagree. npm sees the mismatch and either refuses to install or installs something inconsistent with what the lockfile specifies. This manifests as "Module not found" errors at build time even though npm install appeared to succeed.
Real-World Scenario
Your project runs React 18.2 with Next.js 14. You ask Claude to add a drag-and-drop library. Claude runs npm install @dnd-kit/core@latest, which pulls in a peer dependency on React 19. npm throws ERESOLVE. Claude retries with --legacy-peer-deps, which succeeds — but the installed version of @dnd-kit/core uses React 19 APIs that don't exist in 18.2. Your build passes npm install but crashes at runtime with "Cannot read properties of undefined (reading 'use')" the first time you render a draggable component.
Step-by-Step Fix
Step 1: Read the actual error
npm's ERESOLVE output tells you exactly which packages conflict. Don't skip it:
npm install 2>&1 | head -40
Look for lines like:
npm ERR! Conflicting peer dependency: react@19.0.0
npm ERR! node_modules/react
npm ERR! peer react@"^18.0.0" from the root
This tells you: the root project wants React 18, but something is pulling in React 19.
Step 2: The clean slate install
If the dependency tree is already corrupted, start fresh:
rm -rf node_modules package-lock.json
npm install
This regenerates the lockfile from package.json alone. If package.json itself is the problem (Claude wrote bad version ranges), this won't help — proceed to Step 3.
Step 3: Revert package.json and reinstall with version constraints
If Claude modified package.json with incompatible versions:
git checkout package.json package-lock.json
rm -rf node_modules
npm install
Then install the specific package with an explicit compatible version:
# Check what versions are compatible with your React version:
npm info framer-motion peerDependencies
# Install a specific version known to work with React 18:
npm install framer-motion@11.0.0
Tell Claude:
"I reverted package.json. The project uses React 18.2.
Before installing any package, check its peerDependencies
with `npm info <package> peerDependencies` and install a
version compatible with React 18."
Step 4: When --legacy-peer-deps is the right call
--legacy-peer-deps tells npm to ignore peer dependency conflicts, reverting to v6 behavior. Use it only when:
- You've verified the peer constraint is overly strict (the package works fine with your version despite declaring otherwise)
- The package has no React 18-compatible release and you can't switch libraries
- You're in a monorepo where a transitive dependency has a stale peer constraint
npm install --legacy-peer-deps
Never use --force. It's different from --legacy-peer-deps: --force can overwrite installed packages with conflicting versions, producing a genuinely broken node_modules. --legacy-peer-deps just skips the peer check.
Step 5: Verify the install actually works
npm run build # or: npx tsc --noEmit
A successful install doesn't mean the package works at runtime. Always run the build or type-checker after dependency changes.
Common Pitfalls
- Using
--forceinstead of--legacy-peer-deps.--forcecan silently overwrite packages innode_moduleswith incompatible versions, causing crashes that are hard to trace back to the install step. - Installing
@latestblindly. Always check what@latestrequires before installing it into an existing project.npm info <package> peerDependenciestakes five seconds and prevents most ERESOLVE errors. - Forgetting to regenerate the lockfile. If you manually edit
package.json, runnpm installto updatepackage-lock.json. A stale lockfile causes CI failures that don't reproduce locally. - Letting Claude retry with
--forceafter ERESOLVE. This "fixes" the error message but installs broken dependencies. Always read the ERESOLVE output and address the actual version conflict. - Mixing package managers. If your project uses
pnpmoryarn, don't let Claude runnpm install. The lockfile formats are incompatible and you'll end up with two conflicting lockfiles.
Prevention Checklist
- When asking Claude to install a package, specify your framework version: "Install framer-motion, compatible with React 18.2 and Next.js 14."
- After any dependency change, run
npm run build(ortsc --noEmit) to verify the install works, not just that it succeeded. - Commit
package.jsonandpackage-lock.jsontogether — never one without the other. - Pin major versions in
package.json(use^18.2.0, notlatestor*). - If you're on npm v7+, add
"overrides": {}topackage.jsonto force specific transitive dependency versions when a sub-dependency is the problem. - Tell Claude which package manager your project uses on the first message of a session to prevent it from defaulting to npm.