devkult_
tools29converters34
home/blog/caret-vs-tilde-npm
npmsemverjavascript

^ vs ~ in package.json: npm Version Ranges Explained

Run npm install lodash and your package.json gains a line like "lodash": "^4.17.21". That little caret decides which future versions npm is allowed to install. Its sibling, the tilde, is stricter. Here's exactly what each one allows — including the 0.x case that surprises everyone.

A quick SemVer refresher

A semantic version is MAJOR.MINOR.PATCH:

  • PATCH (4.17.21 → 4.17.22): bug fixes, nothing else should change.
  • MINOR (4.17 → 4.18): new features, backwards compatible.
  • MAJOR (4 → 5): breaking changes.

Ranges are a bet on how well a package's authors follow those rules.

Tilde: patch updates only

~1.2.3 allows newer patch versions within the same minor version:

~1.2.3   means   >=1.2.3 <1.3.0

So 1.2.4 and 1.2.9 are fine; 1.3.0 is not. Leave out parts of the version and the tilde widens: ~1.2 means >=1.2.0 <1.3.0, and ~1 means >=1.0.0 <2.0.0.

Caret: anything that shouldn't break you

^1.2.3 allows newer minor and patch versions within the same major version:

^1.2.3   means   >=1.2.3 <2.0.0

This is npm's default because, if the package follows SemVer, everything below the next major should be safe.

The 0.x surprise

Before 1.0.0, SemVer says anything may change at any time, so the caret gets more cautious. The rule is: the caret never changes the leftmost non-zero number.

Range Allows Doesn't allow
^1.2.3 1.2.3 up to (not including) 2.0.0 2.0.0
^0.2.3 0.2.3 up to 0.3.0 0.3.0
^0.0.3 0.0.3 only 0.0.4
~1.2.3 1.2.3 up to 1.3.0 1.3.0
~0.2.3 0.2.3 up to 0.3.0 0.3.0

So for a 0.x package, ^ behaves much like ~. That's deliberate: in 0.x, a minor bump is allowed to break things.

Pre-releases are opt-in

^1.2.3 will never pull in 1.3.0-beta.1, even though it's numerically in range. A range only matches pre-releases when it names a pre-release of the same MAJOR.MINOR.PATCH itself: ^1.2.3-beta.2 accepts 1.2.3-beta.4 and 1.2.3, but not 1.3.0-beta.1.

The other range forms

Range Means
1.2.3 exactly 1.2.3
>=1.2.3 <1.5.0 any version in that window
1.2.x or 1.2 any 1.2 patch, like ~1.2.0
* or x any version at all
1.2.3 - 2.3.4 inclusive range, >=1.2.3 <=2.3.4
^1.2.3 || ^2.0.0 either range

Why the lockfile matters more

Ranges only decide what npm may install. package-lock.json records what it did install, down to the exact version of every transitive dependency. As long as the lockfile is committed:

  • npm ci installs exactly what the lockfile says, ignoring ranges entirely — use it in CI and deploys.
  • npm install keeps lockfile versions that still satisfy your ranges.
  • Ranges come into play when you run npm update, add a package, or delete the lockfile.

That's why "it works on my machine" bugs so often trace back to an uncommitted or regenerated lockfile, not to ^ versus ~.

What to use

  • Applications: keep the default ^, commit the lockfile, and update deliberately with npm outdated and npm update.
  • Libraries: use ^ for dependencies so your users' installs can share one copy, and make peerDependencies as wide as you've actually tested.
  • Pin exactly only for packages known to break in minor releases — and leave a comment saying why.

Check a range yourself

The semver package that npm itself uses has a command-line tool:

npx semver -r "^0.2.3" 0.2.9 0.3.0 1.0.0
# 0.2.9

It prints only the versions that satisfy the range — a quick way to settle any argument about what ^ will do.

More from the blog