^1.2.3 vs ~1.2.3 in package.json: npm version ranges and the 0.x trap

By default, ^1.2.3 accepts stable releases from 1.2.3 up to but not including 2.0.0; ~1.2.3 accepts 1.2.3 up to but not including 1.3.0. SemVer vs npm, 0.x, prereleases and lockfiles.

DevelopmentnpmSemVerDependency Management

With npm’s default settings, installing a package with npm install <package> saves a range such as "^1.2.3" in package.json. The caret says which later versions npm may install. The similar-looking ~ allows a different range.

SemVer sets the numbering rules; ^ and ~ are npm syntax

Semantic Versioning 2.0.0 writes versions as MAJOR.MINOR.PATCH and says which number a publisher must change.

From 1.0.0 onward, PATCH must increase for backward compatible bug fixes. MINOR must increase when backward compatible functionality is added or public API functionality is marked as deprecated. MAJOR must increase for a backward incompatible change. These are MUST requirements in the specification.

Versions in the 0.y.z series are for initial development. The specification says anything may change at any time and the public API should not be considered stable.

SemVer itself does not define ^ or ~. They are range syntax accepted by npm in package.json. Their precise interpretation comes from node-semver, the library npm uses for version parsing and comparison.

The ranges, expanded

Range Expanded range Stable releases allowed by default
^1.2.3 >=1.2.3 <2.0.0-0 minor and patch changes
~1.2.3 >=1.2.3 <1.3.0-0 patch changes
^0.2.3 >=0.2.3 <0.3.0-0 patch changes
~0.2.3 >=0.2.3 <0.3.0-0 patch changes
^0.0.3 >=0.0.3 <0.0.4-0 no later normal release such as 0.0.4

A caret allows changes that do not modify the left-most non-zero part of major, minor and patch. On 1.x, this makes ^ wider than ~. On 0.x, ^0.2.3 and ~0.2.3 describe the same range. ^0.0.3 does not admit a later normal release such as 0.0.4.

A tilde allows patch changes when the minor version is present. If the minor version is omitted, ~1 expands to >=1.0.0 <2.0.0-0.

The -0 upper bound and prereleases

Under SemVer precedence rules, a prerelease sorts before the normal version with the same number. When prerelease identifiers are compared, numeric identifiers have lower precedence than non-numeric identifiers. 2.0.0-0 therefore sorts before every other valid prerelease of 2.0.0. The upper bound <2.0.0-0 keeps versions such as 2.0.0-beta out of range.

Prereleases inside the numeric range are excluded by default as well. A prerelease can satisfy a comparator set only when at least one comparator in that same set has a prerelease tag with the same [major, minor, patch] tuple. The node-semver includePrerelease option changes this default exclusion.

The following output was produced with Node.js v24.13.0 and node-semver 7.8.5.

const semver = require("semver");

for (const range of ["^1.2.3", "~1.2.3", "^0.2.3", "^0.0.3"]) {
  console.log(range.padEnd(7), new semver.Range(range).range);
}

console.log(semver.satisfies("1.9.0", "^1.2.3"));
console.log(semver.satisfies("1.9.0", "~1.2.3"));
console.log(semver.satisfies("0.3.0", "^0.2.3"));
console.log(semver.satisfies("1.3.0-beta.1", "^1.2.3"));
console.log(
  semver.satisfies("1.3.0-beta.1", "^1.2.3", {
    includePrerelease: true,
  }),
);

Execution output:

^1.2.3  >=1.2.3 <2.0.0-0
~1.2.3  >=1.2.3 <1.3.0-0
^0.2.3  >=0.2.3 <0.3.0-0
^0.0.3  >=0.0.3 <0.0.4-0
true
false
false
false
true

The range is not the installed version

npm’s save-prefix setting defaults to "^". Enabling save-exact saves an exact version instead of a range.

Even with a wide package.json range, the lockfile affects what gets installed. package-lock.json records the exact dependency tree npm generated and is intended to be committed to the repository.

When npm install runs without arguments, it uses the versions in the lockfile while they satisfy the ranges in package.json. If the two files conflict, npm follows package.json, resolves the dependencies again and updates the lockfile.

npm ci requires a lockfile. If package.json and the lockfile do not agree, it exits with an error instead of updating the lockfile. It does not write to package.json or the lockfile.

What a version range does not promise

^1.2.3 accepting 1.9.0 is useful only if the publisher followed SemVer, preserved compatibility within 1.x and increased MAJOR for a breaking change. A version range compares numbers and prerelease rules. It does not test whether the API is actually compatible.

The npm behavior in this article was checked against the npm CLI v12 documentation on October 1, 2026.

Specifications and documentation used

Back to the blog