Semantic Versioning The Numbering System Your npm install Depends On
Semver's three numbers are a contract. MAJOR breaks, MINOR adds, PATCH fixes — and when your build breaks after npm install, nine times out of ten someone ignored that contract. Here's how the numbering system works, what ^ and ~ actually do in package.json, and why committing your lockfile is non-negotiable.
Your build broke. Worked on Friday. npm install on Monday pulled in react-query@5 and now half your hooks are gone. You’re staring at a stack trace that wasn’t there before, and somewhere a changelog is collecting dust.
This is a semver story. Specifically, it’s your fault.
What the three numbers actually mean
MAJOR.MINOR.PATCH — that’s it. Three slots, three rules:
- PATCH (1.2.3 → 1.2.4): Bug fix. Nothing in your code needs to change. You just get less broken behavior.
- MINOR (1.2.3 → 1.3.0): New feature added, backward-compatible. You don’t have to use it, but it’s there.
- MAJOR (1.2.3 → 2.0.0): Something broke. A function was renamed, removed, or changed signature. The old API is gone or works differently.
The key word in all three is backward-compatible. MINOR and PATCH are promises: “we didn’t break anything you were already using.” MAJOR is a warning: “we did.”
When a maintainer bumps MAJOR and you didn’t notice because you pinned ^1.0.0 in package.json and the lockfile was stale — that’s on you. The spec worked exactly as designed.
The semver social contract
Semver is a convention, not a law. Packages can claim to follow it and then ship a MINOR with breaking changes. When that happens, it’s bad faith on the maintainer’s part. But when a package properly bumps MAJOR to signal breakage and you pull it in blindly — you’re the one who broke your own build.
This is why changelogs exist. A CHANGELOG.md entry that says “Removed deprecated v1Api — use v2Api instead” is the maintainer holding up their end. Not reading it is you ignoring yours. The changelog is a two-minute read. The debugging session it prevents is not.
^ vs ~ — the actual mechanics
in package.json, ^ (caret) and ~ (tilde) define version ranges. They look similar and behave very differently.
Caret (^): Allows anything that doesn’t bump MAJOR. This is npm’s default when you run npm install some-package.
^1.2.3resolves to>=1.2.3 <2.0.0^0.2.3resolves to>=0.2.3 <0.3.0— special case:0.xtreats MINOR as breaking^0.0.3resolves to>=0.0.3 <0.0.4—0.0.xpins exactly, no wiggle room
Tilde (~): Allows PATCH updates only, within the specified MINOR.
~1.2.3resolves to>=1.2.3 <1.3.0~1.2resolves to>=1.2.0 <1.3.0— same as~1.2.0~1resolves to>=1.0.0 <2.0.0— equivalent to^1.0.0at this point
Version range examples
| 範囲 | What it allows | Concrete matches |
|---|---|---|
1.2.3 | Exactly this version | Only 1.2.3 |
^1.2.3 | Any MINOR/PATCH ≥ 1.2.3 | 1.2.4, 1.3.0, 1.99.0 — NOT 2.0.0 |
^0.2.3 | PATCH within 0.2.x only | 0.2.4, 0.2.99 — NOT 0.3.0 |
~1.2.3 | PATCH within 1.2.x only | 1.2.4, 1.2.99 — NOT 1.3.0 |
~1.2 | Any patch of 1.2.x | 1.2.0, 1.2.1, 1.2.99 |
>=1.2.3 <2.0.0 | Explicit range | Same result as ^1.2.3 |
1.2.x | Any patch of 1.2 | 1.2.0, 1.2.1, 1.2.99 |
* | Anything at all | Whatever npm feels like installing today |
の * range is a “trust me bro” versioning strategy. You’re pinning nothing. If a library ships v9.0.0 with a fully rewritten API, you’ll get it on the next npm install with a clean cache. Use it only in top-level applications that are never depended on by other packages — and even then, only if reproducibility genuinely doesn’t matter to you (it does).
Pre-release identifiers
Before a stable release, maintainers tag versions with a pre-release label:
1.0.0-alpha.1— early, unstable, API is probably still changing1.0.0-beta.2— feature-complete, still being tested, expect some rough edges1.0.0-rc.1— release candidate, should be production-ready unless something turns up in final testing
Pre-releases sort below the stable release: 1.0.0-alpha.1 < 1.0.0. And critically, ^1.0.0 will ない install 2.0.0-beta.1 — pre-releases only match if you specify them explicitly in your range. This is the behavior that stops you from accidentally opting into an alpha when you meant to track stable releases.
If you’re consuming a package that only has pre-releases, pin the full version string: "some-package": "1.0.0-beta.2". Don’t use ^ または ~ with pre-releases unless you know the maintainer treats them carefully — most don’t.
Checking a range before you commit it
Before pinning a version range in package.json, it’s worth confirming what you’re actually agreeing to install. The Semver Version Calculator takes a version range and a list of candidate versions and shows you which ones match — useful when you’re uncertain whether ~2.3 covers a specific version you need, or when you’re reviewing a PR and the range looks off.
The three failure modes
Most semver-related build failures follow one of three patterns:
^+ MAJOR bump + deleted lockfile: You pinned^1.0.0, maintainer shipped2.0.0, the lockfile was deleted or never committed, CI installs 2.0.0. Fix: commit your lockfile. Every project. No exceptions.*in a library you publish: You’re a library author who used*for a dependency. Every downstream user of your package inherits your wildcard. You’ve made their dependency graph your problem. Fix: use explicit ranges in anything you ship to npm.- Pre-release without a lockfile: A loose range pulled in
1.0.0-alpha.3, the API changed fromalpha.1, nothing works. Fix: pin pre-releases explicitly and — say it with me — commit the lockfile.
Read the changelog
When a MAJOR version ships for anything in your dependency tree, spend two minutes on the changelog. The maintainers wrote it so you wouldn’t have to reverse-engineer the breakage from a stack trace at 3am.
If a library ships breaking changes under a MINOR bump with no changelog — that’s bad faith. File an issue. Call it out publicly. But if the MAJOR was clearly there, the migration guide was detailed, and you pulled it in without looking: the tooling did exactly what you told it to. The contract was written in three numbers. You just didn’t read it.
恵 スコアボードが到着しました!
スコアボード ゲームを追跡する楽しい方法です。すべてのデータはブラウザに保存されます。さらに多くの機能がまもなく登場します!
