# ^ 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](/regex/semver) 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:

```bash
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.
