Difference Between Tilde and Caret in package.json

Dependency updates can move within the version range declared in package.json. Tilde and caret set which versions npm accepts for a dependency.

I’ll show how to compare their version limits and explain how caret works before 1.0.0.

TL;DR: Tilde vs caret in package.json

For a full version such as 1.2.3, tilde stops before 1.3.0, while caret stops before 2.0.0.

Check zero-major ranges and the lockfile before choosing.

Each range uses an inclusive lower bound and exclusive upper bound.

  • For ~1.2.3, the allowed interval starts at 1.2.3 and ends before 1.3.0.
  • With ^1.2.3, the range stops before 2.0.0.
  • Range ^0.2.3 ends before 0.3.0.

What is the difference between tilde and caret in npm ranges?

Both ranges accept versions at or above the declared minimum and exclude an upper boundary set by the operator.

{ “dependencies”: { “example-library”: “^1.2.3” } }

npm’s node-semver package interprets operators such as tilde and caret. The expanded intervals make that behavior concrete.

How to compare a tilde and caret range

Expand each expression to its lower and upper bounds, then compare those endpoints.

Step 1: Read the version tuple

The example uses a major, minor and patch component, compared from left to right.

1.2.3
| | |
| | +-- patch
| +---- minor
+------ major

Step 2: Expand the tilde range

This expression sets the lower bound to 1.2.3 and the exclusive upper bound to 1.3.0.

~1.2.3 = >=1.2.3 <1.3.0

Step 3: Expand the caret range

This expression starts at 1.2.3 and excludes 2.0.0.

^1.2.3 = >=1.2.3 <2.0.0

The first excluded version is the clearest comparison point. I ran the following checks with node-semver from a named JavaScript file:

const cases = [
  ["~1.2.3", ["1.2.9", "1.3.0"]],
  ["^1.2.3", ["1.8.0", "2.0.0"]],
  ["^0.2.3", ["0.2.9", "0.3.0"]],
];
const semver = require("semver");
for (const [range, versions] of cases) {
  console.log(`${range}: ${versions.map(v => `${v}=${semver.satisfies(v, range)}`).join(", ")}`);
}

The actual output confirms that each range accepts the in-bound example and rejects its upper boundary:

~1.2.3: 1.2.9=true, 1.3.0=false
^1.2.3: 1.8.0=true, 2.0.0=false
^0.2.3: 0.2.9=true, 0.3.0=false
Terminal output from node-semver checks of tilde and caret boundaries

An allowed version still needs application tests before you accept the update.

Step 4: Compare the boundaries

RangeLower boundExclusive upper boundMovement
~1.2.31.2.31.3.0Patch
^1.2.31.2.32.0.0Minor and patch

Pick the boundary that matches the updates your tests and release process can handle, then review release notes when changing the resolved version.

What edge cases should you check?

The edge cases are zero-major versions and prereleases, where caret’s boundary rule or prerelease matching changes what qualifies.

How does caret work before 1.0.0?

For example, ^0.2.3 stops before 0.3.0, and ^0.0.3 stops before 0.0.4.

^0.2.3 = >=0.2.3 <0.3.0
^0.0.3 = >=0.0.3 <0.0.4
~0.2.3 = >=0.2.3 <0.3.0

How do prereleases match?

For example, this prerelease range names a prerelease comparator for the 1.2.3 tuple:

^1.2.3-beta.2

node-semver can match later prereleases on that same tuple. The SemVer specification gives prereleases lower precedence than the associated final version.

What does package-lock.json record?

package.json declares the dependency range; package-lock.json records npm’s resolved dependency tree. With a compatible lockfile, npm install generally follows that recorded resolution. Use npm update to seek newer versions within the ranges already declared, then inspect the lockfile diff and run project tests.

npm install
npm update example-library

For an application, commit the reviewed lockfile so teammates and deployment jobs use the same resolved tree. Library policy can differ.

Follow the library’s release workflow.

What to do after choosing a range

Check the declared range and resolved version before merging a dependency update.

ReviewInspect
Allowed releasesDependency entry in package.json
Selected treepackage-lock.json diff
BehaviorRelease notes and project tests

For related context, read CodeForGeek’s npm guide and its explanation of why npm install rewrites package-lock.json. The primary references are node-semver and the SemVer specification.

Conclusion

Use the expanded bounds to compare tilde and caret, treat zero-major ranges on their own terms, and review the lockfile when npm resolves an update.

FAQ

Each option has a different upper boundary for a full version.

Is caret or tilde safer?

Neither guarantees compatibility. Tilde narrows the allowed range for a full version, while caret permits changes to its protected boundary. Review and test updates.

Should I commit package-lock.json?

For an application, committing the reviewed lockfile helps reproduce the resolved dependency tree across development and deployment.

Does npm update change the range?

npm update seeks newer resolved versions within the ranges already declared. Change the package.json specification separately when you intend to change the range.

Devdeep Ghosh
Devdeep Ghosh

Devdeep Ghosh covers Node.js development, including npm workflows, package configuration, Express troubleshooting, and server-side APIs. His work also covers concurrency and module systems.

Articles: 10