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:
# 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.0That 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.
# 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-auditI keep a small shell function in my .zshrc for this exact scenario:
# 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.
# 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.
# 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'tThe 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:
# 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 installStep 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:
For native module failures specifically:
# 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-essentialPrevention: 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
enginesin package.json. This tells npm to fail fast if someone (including future you) tries to install with the wrong Node version.
{
"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 testwill 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.
