Back to System Design
System Design
easy
junior

What is the difference between caret and tilde in package.json version ranges?

`^1.2.3` allows any 1.x.x ≥ 1.2.3 (compatible-with-1.2.3). `~1.2.3` allows any 1.2.x ≥ 1.2.3 (patch only). Both stop at the next significant boundary; the difference is whether minors are allowed.

4 min read·~6 min to think through

npm uses semver for version ranges. The leading symbol controls how loose the range is.

RangeAllowsLocks
1.2.3exactly thatmajor, minor, patch
~1.2.3>=1.2.3 <1.3.0major, minor
^1.2.3>=1.2.3 <2.0.0major
^0.2.3>=0.2.3 <0.3.0 (special!)major + minor (because 0.x is unstable)
*anythingnothing

^ is the npm default and the right starting point for most dependencies — semver promises no breaking changes within a major. ~ is stricter: only patches. Use it for libraries with shaky minor bumps or for tightly-coupled internal packages.

Beyond ^ and ~, the lockfile (package-lock.json / yarn.lock / pnpm-lock.yaml) is what actually pins exact versions across installs. Without a lockfile, ^1.2.3 could resolve to a different version on each npm install, which is what causes "works on my machine" version drift.

Code

ts
// "^1.2.3"  -> 1.2.3, 1.5.0, 1.99.99    NOT 2.0.0
// "~1.2.3"  -> 1.2.3, 1.2.9                NOT 1.3.0
// "^0.2.3"  -> 0.2.3, 0.2.99               NOT 0.3.0   (0.x is special)
// "1.2.3"   -> only 1.2.3
// ">=1.2.3" -> any version >=1.2.3 (no upper bound — risky)
Effective ranges

Follow-up questions

  • What's the role of package-lock.json given that ranges are loose?
  • How does pnpm or Yarn Berry change the dependency-resolution model?
  • What does `npm ci` do differently from `npm install`?

Common mistakes

  • Committing without a lockfile and getting different installs on CI vs dev.
  • Believing `^0.x.y` allows any 0.x — it's restricted to the same minor.
  • Trusting semver to be perfectly honored — many libraries break minor bumps in practice.

Performance considerations

  • Lockfiles speed installs by skipping resolution — `npm ci` is faster than `npm install` for CI.

Edge cases

  • Pre-releases (`1.2.3-beta.1`) are excluded from `^`/`~` unless you opt in with `--include=prerelease`.
  • Workspaces (monorepos) often pin internal packages to `workspace:*` instead of semver.

Real-world examples

  • Most boilerplates use `^` so dependencies pick up bug fixes; security-critical projects sometimes pin exact versions instead.

Senior engineer discussion

Senior signal: discuss how lockfiles, peer deps, hoisting, and dedupe interact, and the case for exact pinning + Renovate / Dependabot vs. range-based.