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.0So 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.01.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:
| Range | Means | Allows |
|---|---|---|
^1.2.3 | >=1.2.3 <2.0.0 | Minor and patch |
^0.2.3 | >=0.2.3 <0.3.0 | Patch only |
^0.0.3 | >=0.0.3 <0.0.4 | Nothing but 0.0.3 |
~1.2.3 | >=1.2.3 <1.3.0 | Patch only |
~1.2 | >=1.2.0 <1.3.0 | Patch only |
~1 | >=1.0.0 <2.0.0 | Minor 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 ~:
| Syntax | Example | Means |
|---|---|---|
| Exact | 1.2.3 | Only 1.2.3 |
| Comparators | >=1.2.0 <1.5.0 | Both conditions (AND) |
| OR | ^1.0.0 || ^2.0.0 | Either range |
| X-range | 1.2.x or 1.x | Any value in that position |
| Hyphen | 1.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.0Numeric 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.yamloryarn.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.3allows minor and patch updates:<2.0.0.~1.2.3allows patch updates only:<1.3.0.- Below 1.0.0, caret narrows:
^0.2.3is 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.