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 --force instead of --legacy-peer-deps. --force can silently overwrite packages in node_modules with incompatible versions, causing crashes that are hard to trace back to the install step.
  • Installing @latest blindly. Always check what @latest requires before installing it into an existing project. npm info <package> peerDependencies takes five seconds and prevents most ERESOLVE errors.
  • Forgetting to regenerate the lockfile. If you manually edit package.json, run npm install to update package-lock.json. A stale lockfile causes CI failures that don't reproduce locally.
  • Letting Claude retry with --force after 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 pnpm or yarn, don't let Claude run npm 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 (or tsc --noEmit) to verify the install works, not just that it succeeded.
  • Commit package.json and package-lock.json together — never one without the other.
  • Pin major versions in package.json (use ^18.2.0, not latest or *).
  • If you're on npm v7+, add "overrides": {} to package.json to 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.