Tags: web-dev concept

Semantic Versioning

Date: 2026-08-17


Three numbers promising what an upgrade will do to you. The promise is made by the publisher about their intent, and experienced by you as whether your build still works — and those two things diverge routinely.


Semantic versioning (semver) encodes the nature of a change in the version number:

MAJOR . MINOR . PATCH
  2   .   4   .   1
BumpMeans
MajorA breaking change
MinorNew functionality, backwards compatible
PatchA bug fix, backwards compatible

The promise: you can take any minor or patch upgrade without reading the changelog.

Where the promise fails

“Breaking” is the publisher’s judgement about their intended contract. Consumers depend on behaviour the publisher never considered part of it.

Hyrum’s law names the problem: with enough consumers, every observable behaviour of your system will be depended on by somebody, regardless of what you documented.

Patches that broke things, all real categories:

  • A bug fix — someone had worked around the bug
  • A performance change — someone depended on the timing
  • A changed error message — someone parsed it
  • New validation — someone was sending invalid input that used to pass
  • A changed sort order — nobody promised it, everyone relied on it
  • A new peer dependency requirement

None of these are the publisher acting badly. They’re the gap between a documented contract and an observed one.

Ranges, and what they let in

2.4.1     exact — only this
~2.4.1    patches      → 2.4.x
^2.4.1    minor+patch  → 2.x.x
>=2.4.1   anything above
*         anything     → never

^ is npm’s default, so npm install some-package accepts every future minor release. A fresh install can pull code published minutes ago into your build.

The 0.x exception catches people: below 1.0, the caret behaves more strictly, because semver treats pre-1.0 minor bumps as potentially breaking.

^0.4.1   →  0.4.x only, NOT 0.5.0
^1.4.1   →  1.x.x

A large share of the ecosystem sits below 1.0 indefinitely, which means the caret is stricter than most people assume in exactly those cases.

Pre-release and build metadata

2.5.0-beta.1      pre-release — sorts BEFORE 2.5.0
2.5.0-rc.2
2.5.0+build.7     build metadata — ignored in
                  comparison

Pre-release versions are excluded from ranges by default, so ^2.4.1 will not install 2.5.0-beta.1. That’s usually what you want and occasionally surprising.

Where the lockfile comes in

Semver describes what’s permitted; the lockfile records what happened.

package.json    ^2.4.1     the permission
lockfile        2.4.7      the fact

The range is what an install is allowed to choose; the lockfile is what it chose. Reproducibility comes from the second, not the first — Lockfiles.

Practical position

  • Applications: pin via the lockfile, and use ranges in package.json. Upgrade deliberately
  • Libraries: use ranges. Pinning exactly forces version conflicts on everyone who depends on you
  • Read changelogs for majors, diff the lockfile for the rest
  • Automate patch and minor upgrades with tests as the gate. Six months of deferred upgrades batched into one afternoon is how upgrades stop happening — Dependency Management
  • Treat semver as a signal, not a guarantee. Your tests are the guarantee

Date-based versioning, the alternative

Increasingly common for platform APIs, and it sidesteps the judgement problem entirely:

2026-04

There’s no argument about whether a change is major or minor — there’s a release, a support window, and consumers pin a date and migrate on a schedule. Shopify, Stripe and Adobe all work this way — Shopify, Backwards Compatibility.

It works because the publisher stops having to classify changes, which is precisely the step semver gets wrong.