Semver: ^1.2.3, ~1.2.3, and when each matters
MAJOR.MINOR.PATCH
1.4.2 β β βββ PATCH: bug fixes, no behaviour change β βββββ MINOR: new features, backward-compatible βββββββ MAJOR: breaking changes
The contract is: bumping PATCH or MINOR shouldn't break your code. Bumping MAJOR can. Libraries that take semver seriously are trustworthy to keep up-to-date with.
Range operators in package.json
"react": "^18.2.0", // 18.x.x β any MINOR or PATCH within MAJOR 18 "date-fns": "~3.6.0", // 3.6.x β any PATCH within MINOR 3.6 "vite": "5.4.2", // exactly 5.4.2 β no flexibility "axios": ">=1.0.0", // anything 1.0.0 or higher
^ is the default npm install writes. It allows up to (but not including) the next MAJOR. That's the right answer for libraries that take semver seriously.
When to pin exactly
"some-flaky-lib": "1.4.2"
Use exact versions when:
- The library is known to break semver (you've been burned before).
- You're producing a binary or container image and care about byte-reproducible builds.
- You're debugging an upgrade and want to bisect.
The lock file rule
Even with ^, your package-lock.json (or pnpm-lock.yaml) pins exact versions. Two devs running npm install get identical bytes β that's what the lock file enforces. The ^ only affects what happens on npm update (or a fresh install with no lock).
"Why does npm install some-pkg@latest break me?"
Because @latest ignores the range in package.json. It installs whatever the package's current head is, even across MAJOR boundaries. Use npm install some-pkg (no @latest) to respect the range.
Sign up to start coding
Theory is open to everyone. The interactive editor, live preview, and check are unlocked with a 7-day free trial β card required, cancel anytime.
Sign up β free trial βFirst 10 lessons in each track are free. No card needed for those.