AuthonAuthon Blog
debugging6 min read

How to Fix Dependency Rot in Projects You Haven't Touched in Months

Fix broken builds from dependency rot when revisiting old projects. Step-by-step debugging guide for stale Node.js and Python dependencies.

AW
Alan West
Authon Team
How to Fix Dependency Rot in Projects You Haven't Touched in Months

So the dev job market is warming back up — postings are up 15% since mid-2025 according to FRED data. If you're anything like me, that means you're dusting off side projects and portfolio repos that have been sitting untouched. And then you run npm install and everything explodes.

Dependency rot is real, and it's one of the most frustrating problems in modern development. You didn't change a single line of code, but suddenly nothing builds.

Why This Happens

The root cause is deceptively simple: your lockfile pins exact versions, but the ecosystem around those versions keeps moving. Here's what actually goes wrong:

  • Transitive dependencies get yanked or deprecated. A package three levels deep in your tree publishes a breaking change or gets removed entirely.
  • Node/Python/Ruby runtime versions drift. Your project expected Node 18, but your machine now runs Node 22. Native bindings compiled against the old version won't load.
  • Registry metadata changes. Package registries occasionally restructure URLs, deprecate old endpoints, or enforce new authentication requirements.
  • OS-level libraries update. That OpenSSL or libsqlite upgrade you didn't notice? It just broke a native module.

The frustrating part is that the error messages rarely point to the actual cause. You'll see something like:

bash
# The classic unhelpful error
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! Found: react@18.2.0
npm ERR! node_modules/react
npm ERR!   react@"^18.2.0" from the root project
npm ERR!   peer react@">=18" from some-library@4.1.0

That tells you what failed, but not why it was fine six months ago and broken now.

Step 1: Audit Before You Touch Anything

Don't start deleting node_modules and reinstalling yet. First, understand the damage.

bash
# Check what's actually outdated
npm outdated

# For a deeper look at vulnerabilities
npm audit

# If you're using Python, pip-audit is solid
pip install pip-audit
pip-audit

I keep a small shell function in my .zshrc for this exact scenario:

bash
# Quick health check for a stale project
project-checkup() {
  echo "=== Runtime Versions ==="
  node -v 2>/dev/null && echo "Expected: $(cat .nvmrc 2>/dev/null || echo 'no .nvmrc')"
  python3 --version 2>/dev/null && echo "Expected: $(cat .python-version 2>/dev/null || echo 'no .python-version')"

  echo "\n=== Dependency Health ==="
  if [ -f package.json ]; then
    echo "Outdated packages:"
    npm outdated 2>/dev/null | head -20
    echo "\nVulnerabilities:"
    npm audit 2>/dev/null | tail -5
  fi

  if [ -f requirements.txt ]; then
    echo "Stale pins:"
    # Show packages that are more than one major version behind
    pip list --outdated 2>/dev/null | head -20
  fi

  echo "\n=== Lockfile Status ==="
  git diff --stat HEAD -- '*.lock' 'package-lock.json' 2>/dev/null
}

This gives you a snapshot before you start breaking things further.

Step 2: Fix Your Runtime First

The single most common cause of "it worked before" is a runtime version mismatch. Fix this before touching dependencies.

bash
# If you have .nvmrc or .node-version
nvm install  # installs the version specified in .nvmrc
nvm use

# If you don't have a version file, check your lockfile
# The lockfile metadata often records what version generated it
head -5 package-lock.json
# Look for: "lockfileVersion": 2 means npm 7-8 (Node 16-18 era)
# "lockfileVersion": 3 means npm 9+ (Node 18+ era)

For Python projects, pyenv solves the same problem. I've lost count of how many times a project broke because my system Python jumped from 3.10 to 3.12 and some C extension wasn't compatible yet.

Step 3: The Nuclear Option (Done Right)

Okay, now you can delete and reinstall. But do it properly.

bash
# JavaScript/Node projects
rm -rf node_modules
rm package-lock.json  # yes, really — if it's deeply broken
npm install

# If you want to preserve your lock as much as possible instead:
rm -rf node_modules
npm ci  # installs exactly what the lockfile says, fails if it can't

The difference matters. npm ci is strict — it either reproduces your lockfile exactly or fails. npm install will resolve fresh and update the lockfile. If npm ci fails but npm install works, your lockfile was the problem.

For Python, the equivalent dance is:

bash
# Recreate your virtual environment from scratch
rm -rf .venv
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# If using poetry:
poetry env remove python
poetry install

Step 4: Handle the Stubborn Failures

Sometimes a clean install still fails because a specific package genuinely doesn't work anymore. Here's my debugging flow:

  • Read the actual error — scroll up past the npm summary to the real build output
  • Check if the package is maintained — visit the repo, look at the last commit date
  • Search issues for your error — someone else hit this, guaranteed
  • Check if a fork exists — abandoned packages often get community forks
  • For native module failures specifically:

    bash
    # Rebuild native modules against your current runtime
    npm rebuild
    
    # If that fails, the nuclear option for native deps
    npm rebuild --build-from-source
    
    # For node-gyp issues specifically, make sure build tools are current
    # macOS:
    xcode-select --install
    # Ubuntu/Debian:
    sudo apt-get install build-essential

    Prevention: Stop This From Happening Again

    Here's what I do now on every project, and it's saved me hours:

    • Pin your runtime version. Add .nvmrc, .python-version, or .tool-versions (if you use asdf). No excuses.
    • Set up Dependabot or Renovate. Even on side projects. Renovate is free and can auto-merge patch updates. You'll get small, digestible PRs instead of one massive upgrade shock.
    • Use engines in package.json. This tells npm to fail fast if someone (including future you) tries to install with the wrong Node version.
    json
    {
      "engines": {
        "node": ">=20.0.0 <23.0.0",
        "npm": ">=10.0.0"
      }
    }
    • Run CI on a schedule. A weekly cron job on your CI pipeline that just runs npm ci && npm test will catch rot before it compounds. Most CI platforms support scheduled workflows at no extra cost.

    The Bigger Picture

    With the job market picking back up, a lot of developers are revisiting old projects — whether for portfolio polish, interview prep, or just getting back into the rhythm. Don't let dependency rot be the thing that kills your momentum. Spend an afternoon getting your toolchain solid, set up the prevention guardrails, and you won't have to fight this battle again in six months.

    The irony of modern development is that doing nothing to a project is itself a form of technical debt. Code doesn't decay, but the ecosystem it depends on never stops moving.

    How to Fix Dependency Rot in Projects You Haven't Touched in Months | Authon Blog