Skip to content
DDevToolery

2 April 2025 · 6 min read

What ^ and ~ actually let into your build

The two most common version range operators, and the assumption they both rest on.

Semantic versioning gives three numbers a contract. MAJOR changes when something breaks, MINOR when something is added compatibly, PATCH when something is fixed compatibly. Range operators are shorthand for how much of that contract you are willing to trust.

The caret

^1.2.3 allows anything from 1.2.3 up to but excluding 2.0.0. New features and fixes come in automatically; breaking changes do not. This is npm's default and the reason a fresh install today can differ from one last month.

The tilde

~1.2.3 allows 1.2.3 up to but excluding 1.3.0 — patches only. Tighter, and appropriate when a minor release has previously broken you.

^1.2.3  →  >=1.2.3 <2.0.0
~1.2.3  →  >=1.2.3 <1.3.0
 1.2.3  →  exactly 1.2.3

The zero rule

Below 1.0.0 the spec says nothing is stable, and npm narrows the caret accordingly: ^0.2.3 allows only 0.2.x, and ^0.0.3 allows only 0.0.3. So a caret on a 0.x dependency behaves like a tilde, or like a pin. This surprises people who assume the operator means one thing everywhere.

Every range operator rests on one assumption: that the maintainer classified their change correctly. Nothing enforces that. A breaking change shipped as a patch reaches you regardless of how careful your range is.

Which is why lockfiles exist

package-lock.json, yarn.lock and pnpm-lock.yaml record the exact version actually installed, along with an integrity hash. The range in package.json says what is permitted; the lockfile says what happened. Commit it, and use npm ci rather than npm install in CI so the lockfile is honoured exactly.

Comparing versions correctly

Each part is compared as a number, not as text. That is why 1.10.0 is newer than 1.9.9 even though it sorts earlier alphabetically — a bug that appears whenever someone sorts version strings with a plain string comparison.

Prereleases

A prerelease has lower precedence than the release it precedes: 1.0.0-rc.1 comes before 1.0.0. Prerelease identifiers compare part by part, numerically where numeric, so rc.2 beats rc.10 only if you compare them as strings — which the spec explicitly does not.

Build metadata

Anything after a plus sign is ignored entirely for precedence. 1.0.0+build.1 and 1.0.0+build.99 are the same version as far as any comparison is concerned.

Practical advice

  • Use carets for libraries you trust and update often.
  • Use tildes or exact pins for anything that has broken you before, and for build tooling.
  • Commit the lockfile and install from it in CI.
  • Read the changelog on a major bump rather than assuming the number tells you enough.
  • If you publish a package, take the contract seriously — someone else's caret is a promise you made.

Tools mentioned