Safeguard
Developer Tools•

Caret vs tilde in package.json: what each version range allows

A caret range accepts minor and patch releases, a tilde range accepts patch releases only. See how each behaves, including the 0.x exception.

Safeguard Security Team
3 min read

A caret range such as ^1.0.4 accepts new minor and patch releases within the same major version. A tilde range such as ~1.0.4 accepts patch releases only. Both depend on publishers following semantic versioning honestly, and neither is a promise that an update is safe, only that it is allowed.

The version number in one minute

npm documents versions as MAJOR.MINOR.PATCH. A patch bump is for backward compatible bug fixes, a minor bump is for backward compatible new features, and a major bump is for changes that break compatibility.

That is the contract. Ranges are how you tell the installer which parts of the contract you trust.

What each range means

The npm documentation lists these forms for package.json:

  • ~1.0.4 accepts patch releases only, so 1.0.5 but not 1.1.0.
  • ^1.0.4 accepts minor releases, so 1.1.0 and 1.9.2 but not 2.0.0.
  • 1.0.x and 1.0 also accept patch releases.
  • 1.x and 1 also accept minor releases.
  • * or x accepts any version, including new majors.
  • A bare 1.0.4 means exactly that version.

The 0.x exception

Versions below 1.0.0 get narrower treatment. The npm update documentation shows that a caret on 0.2.0 only reaches versions from 0.2.0 up to but not including 0.3.0, and a caret on 0.4.0 can reach 0.4.1 but not 0.5.0.

In practice, for a package that is still at 0.x, a minor bump is treated as potentially breaking. If you depend on many 0.x packages, the caret gives you less automatic movement than you may expect.

Why ranges are only half the story

A range says what could be installed. The lockfile says what was installed. With a committed package-lock.json, a plain install keeps using the locked versions until you ask for an update, so a caret range does not silently change your build from one day to the next.

Without a lockfile, or when someone deletes it, the same range can resolve to a different version tomorrow. That includes a freshly published version you have not reviewed.

Which one should you choose

There is no universal answer, and the Renovate documentation makes the point that minor releases often carry fixes too, so a tilde does not reliably keep change away. Some ways to think about it:

  • If you pin exact versions and let an update bot open pull requests, the range matters little.
  • If you use ranges, rely on the lockfile for reproducibility and on CI for catching breakage.
  • For packages you publish, broad ranges on dependencies are kinder to the people who install you.

Whatever you choose, make it a team decision rather than a side effect of the default. Check what your package.json says, and check that package-lock.json is committed.

For a view of what actually resolved across a project, Safeguard's SBOM and dependency scanning lists the installed versions and their known issues. You can try a free scan at gold.safeguard.sh/scan. For the trade-offs of exact versions, see dependency pinning vs ranges.

Quick check

Does ^1.2.3 ever install 2.0.0?

No. A caret range stays within the same major version, so it can reach 1.9.9 but not 2.0.0.

Does a range change my build if I have a lockfile?

Not on a normal install. The lockfile fixes the resolved versions until you run an update or change the manifest.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.