Semver Ranges: Caret (^) vs Tilde (~) in package.json

What ^1.2.3 and ~1.2.3 actually allow in package.json, why 0.x versions behave differently, and how semver decides that 1.10.0 is newer than 1.9.0.

· 3 min read

Open almost any package.json and you'll see versions like ^18.3.1 or ~4.17.21. Those little symbols decide which updates npm install is allowed to pull in, and getting them wrong is a classic source of "it worked yesterday" bugs. Here's what each range means, with the edge cases spelled out.

Semantic versioning in one minute

A semantic version has three numbers: MAJOR.MINOR.PATCH.

  • MAJOR goes up for breaking changes.
  • MINOR goes up for new features that don't break anything.
  • PATCH goes up for bug fixes.

It can also carry a pre-release tag, as in 2.0.0-beta.1, and build metadata, as in 1.0.0+build.5. Each number is compared as a number, not as text, so 1.10.0 is newer than 1.9.0.

Caret (^): allow minor and patch updates

^1.2.3 means "anything compatible with 1.2.3", which semver defines as not changing the leftmost non-zero number:

^1.2.3  →  >=1.2.3 <2.0.0

So 1.2.4, 1.3.0 and 1.99.0 all match, but 2.0.0 doesn't. This is npm's default: npm install lodash writes a caret range into package.json.

Tilde (~): allow patch updates only

~1.2.3 is stricter. It allows changes to the patch number only:

~1.2.3  →  >=1.2.3 <1.3.0

1.2.9 matches, 1.3.0 doesn't. Use tilde when a dependency has a habit of slipping behavior changes into minor releases.

The 0.x exception

Below 1.0.0, semver says anything may change at any time, so caret ranges get narrower to stay safe:

RangeMeansAllows
^1.2.3>=1.2.3 <2.0.0Minor and patch
^0.2.3>=0.2.3 <0.3.0Patch only
^0.0.3>=0.0.3 <0.0.4Nothing but 0.0.3
~1.2.3>=1.2.3 <1.3.0Patch only
~1.2>=1.2.0 <1.3.0Patch only
~1>=1.0.0 <2.0.0Minor and patch

This catches people out. ^0.4.0 won't install 0.5.0, even though a caret on a 1.x version would happily jump several minor releases.

You can test any version against any range with the semver comparator. Enter 0.5.0 and ^0.4.0, and it shows that the version doesn't satisfy the range.

Other range syntax

npm supports more than ^ and ~:

SyntaxExampleMeans
Exact1.2.3Only 1.2.3
Comparators>=1.2.0 <1.5.0Both conditions (AND)
OR^1.0.0 || ^2.0.0Either range
X-range1.2.x or 1.xAny value in that position
Hyphen1.2.3 - 2.3.4>=1.2.3 <=2.3.4, inclusive
Any*Any version (avoid this)

How pre-releases compare

A pre-release ranks below its release: 1.0.0-rc.1 < 1.0.0. Pre-release identifiers are compared one dot-separated piece at a time:

1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0

Numeric pieces compare as numbers, so beta.11 is newer than beta.2, and text pieces compare alphabetically. Build metadata (+build.5) is ignored entirely when comparing.

Ranges also skip pre-releases by default. ^1.0.0 won't match 1.1.0-beta.1, so a beta can't sneak into a normal install.

Which should you use?

  • Applications: the default caret is fine, as long as you commit your lockfile (package-lock.json, pnpm-lock.yaml or yarn.lock). The lockfile pins exact versions. The range only matters when you add or update packages.
  • Libraries: use caret ranges for dependencies so your users don't end up with duplicate copies of everything.
  • Fragile or 0.x dependencies: use tilde or an exact version, and update deliberately.

Comparing versions in code

If you need this logic in a script, use the official semver package rather than comparing strings:

import semver from 'semver';

semver.gt('1.10.0', '1.9.0');           // true
semver.satisfies('1.3.0', '~1.2.3');     // false
semver.satisfies('0.3.0', '^0.2.3');     // false
semver.maxSatisfying(['1.2.3', '1.4.0', '2.0.0'], '^1.2.0'); // '1.4.0'

Comparing version strings directly ('1.10.0' > '1.9.0') returns false, because strings compare character by character.

Quick reference

  • ^1.2.3 allows minor and patch updates: <2.0.0.
  • ~1.2.3 allows patch updates only: <1.3.0.
  • Below 1.0.0, caret narrows: ^0.2.3 is patch-only.
  • Pre-releases rank below their release and are skipped by ranges unless you ask for them.

Not sure whether a version fits a range? Check it in the semver comparator.