Semantic Versioning
Every version on npm is a semver string: MAJOR.MINOR.PATCH. The genius of the scheme is that the number itself communicates a promise about compatibility — so tools (and humans) can reason about which upgrades are safe. The range operators ^ and ~ then encode how much of that promise you are willing to trust automatically.
The three numbers
Position | Bumped when… | Promise |
|---|---|---|
| You make a breaking change | Existing code may break — read the changelog |
| You add a backward-compatible feature | Safe to upgrade; new things added |
| You make a backward-compatible bug fix | Safe to upgrade; behavior only fixed |
Range operators
Range | Means | Matches | Excludes |
|---|---|---|---|
| Compatible — no MAJOR bump | 1.2.3 → <2.0.0 | 2.0.0 |
| Approximately — no MINOR bump | 1.2.3 → <1.3.0 | 1.3.0 |
| Exact — this and only this | 1.2.3 | everything else |
| This or anything newer | 1.2.3 → ∞ | < 1.2.3 |
| Any version with MAJOR 1 | 1.0.0 → <2.0.0 | 2.0.0 |
| Any version at all | everything | nothing |
^ (caret) is npm's default — npm install express writes ^4.19.2. It means "let me get bug fixes and new features automatically, but never a breaking major."
Caret vs tilde, visually
Which updates each range allows
Installed: 1.2.3 Available: 1.2.4 1.3.0 2.0.0 ^1.2.3 ► accepts 1.2.4 ✓ 1.3.0 ✓ 2.0.0 ✗ (locks MAJOR) ~1.2.3 ► accepts 1.2.4 ✓ 1.3.0 ✗ 2.0.0 ✗ (locks MAJOR.MINOR) 1.2.3 ► accepts nothing — exact pin
Ranges vs the lockfile
A range in package.json says what you allow; the lockfile records what you got. They work together:
package.json: "express": "^4.19.2" ← the policy (allow 4.x ≥ 4.19.2) package-lock.json: express 4.19.2 ← the fact (exactly this, today)
Until you run npm update (or bump the range), the lock keeps you on 4.19.2 even after 4.20.0 ships. The range only matters when the lock is regenerated. See Understanding package-lock.json.
Pre-release and build tags
Versions can carry a pre-release suffix after a hyphen. These sort before the matching stable release and are excluded from normal ranges unless you opt in:
2.0.0-alpha.1 < 2.0.0-beta.2 < 2.0.0-rc.1 < 2.0.0 # ^2.0.0 does NOT match 2.0.0-alpha.1 — you must ask explicitly: npm install pkg@2.0.0-beta.2
Testing a range
npx semver 4.20.0 -r "^4.19.2" # prints 4.20.0 → it satisfies the range npx semver 5.0.0 -r "^4.19.2" # prints nothing → it does not
Practical policy
Default to
^for most dependencies — automatic fixes and features, no surprise majors.Use
~when you want only patch fixes (conservative; common for fragile or fast-moving packages).Use an exact pin for tools where reproducibility trumps everything (or rely on the lockfile, which pins anyway).
Treat a MAJOR bump as a task: read the migration guide, run tests — never auto-merge it.