SemVer Cheatsheet - Semantic Versioning Reference
A careless constraint like ^ or ~ silently decides whether your deploy upgrades dependencies or breaks the build. This reference covers MAJOR.MINOR.PATCH structure, prerelease tags, comparison operators, and the npm ranges you actually write (^ ~ >= < * and advanced combos). Use it to write a package.json that upgrades exactly what you want, and to tell why npm picked version X. After reading you read and write version ranges with intent.
Typical Use Case
For developers maintaining npm packages, CLI tools and dependency pinning. Semantic versioning splits a version into MAJOR.MINOR.PATCH, where each increment signals a different kind of change; this sheet also maps changes to segments (feat → MINOR, fix → PATCH, BREAKING CHANGE → MAJOR). The lower half is the real point: how to write range expressions safely. ^1.2.3 permits the whole 1.x line while ~1.2.3 stays inside 1.2.x — a one-order-of-magnitude difference at the boundary. Pre-release versions and build metadata follow separate comparison rules. Pin exactly with =1.2.3 or the range 1.2.3 - 2.3.4.
Version Number Structure 5
MAJOR.MINOR.PATCH1.2.3MAJOR: breaking API changeMINOR: new backward-compatible featuresPATCH: backward-compatible bug fixesPre-release & Build Metadata 6
1.0.0-alpha.11.0.0-beta.21.0.0-rc.1alpha < beta < rc < final1.0.0-alpha < 1.0.0-alpha.11.0.0+20260721Version Comparison Operators 5
=1.2.3>1.2.3>=1.2.3<2.0.0<=1.9.9npm Version Range Symbols 7
^1.2.3~1.2.31.2.3 - 2.3.41.x1.2.x*latestnpm Advanced Range Syntax 5
1.2.3 || >=2.0.0>=1.2.3 <2.0.0~1.2 || ^2.0.0github:owner/repofile:./local-pkgVersion Changes & Release Rules 5
feat: new featurefix: bug fixBREAKING CHANGE: breaking change0.x.x1.0.0Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
MAJOR.MINOR.PATCH | the three segments, mapping to incompatible change, compatible feature and compatible fix | 1.2.3 :: major 1, minor 2, patch 3 |
^1.2.3 | compatible range: every 1.x patch and minor, blocked before 2.0.0 | ^1.2.3 :: allows 1.x.x, rejects 2.0.0 (compatible version) |
~1.2.3 | close range: only 1.2.x is allowed, so 1.3.0 is rejected | ~1.2.3 :: allows 1.2.x, rejects 1.3.0 (close version) |
pre-release tag alpha/beta/rc | a pre-release ranks below the final release; the same tag compares by its number | 1.0.0-alpha < 1.0.0-alpha.1 |
+ build metadata | records build info while playing no part at all in precedence comparison | 1.0.0+202****0721 |
range and or operators | a space means AND, a double pipe means OR, covering needs that span major versions | ~1.2 || ^2.0.0 |
commit message prefix | lets conventional commits drive the next version number automatically | BREAKING CHANGE: a breaking change :: bumps MAJOR |
Common pitfalls
SymptomA pre-release like 1.0.0-alpha was installed even though 1.0.0 was expected to win.
CauseA version with a pre-release tag ranks below the final release, and pre-releases are not selected automatically.
FixWrite 1.0.0-alpha explicitly to get it; ship 1.0.0 rather than continuing on alpha.
SymptomTwo versions differing only in build number are treated as distinct in precedence, or as identical when they are not.
CauseBuild metadata after the + sign is an identifier only and takes no part in precedence comparison.
FixUse the + suffix to distinguish artifacts, and compare precedence using only the three segments plus any pre-release tag.
SymptomCI breaks one day out of nowhere because upstream published a breaking release.
Cause* accepts anything and ^ allows the whole minor line, so a MAJOR bump upstream introduces incompatible changes.
FixPin in CI with a lock or exact range such as 1.2.3 - 2.3.4, and reserve ^ for application code.
SymptomBehavior of a 0.x dependency changed after a patch-level install.
CauseA major version of 0 signals an unstable API, so even a MINOR bump can break compatibility.
FixConstrain 0.x dependencies with ~0.x.y so the range stays inside one minor line.
Troubleshooting
1See which versions a range expression actually covers
npm view lodash@'^1.2.3' versions --jsonThe printed list is what the range resolves to; versions outside it mean the range is too narrow or malformed.
2Confirm the installed version matches the declared dependency tree
npm ls --depth=0An invalid marker or UNMET DEPENDENCY means the locked version conflicts with the declared range.
3Derive the next version number from commit messages
npx standard-version --dry-rundry-run only prints which segment feat, fix and BREAKING CHANGE would bump, writing nothing.
Command Examples
Ordering prereleases against releases by SemVer rules
node -e "
function cmp(a, b) {
const A = a.split(/[.+-]/), B = b.split(/[.+-]/);
const pa = a.includes('-'), pb = b.includes('-');
for (let i = 0; i < 3; i++) { const d = +A[i] - +B[i]; if (d) return d; }
if (pa !== pb) return pa ? -1 : 1;
if (pa && pb) {
const x = a.split('-').slice(3), y = b.split('-').slice(3);
for (let i = 0; i < Math.max(x.length, y.length); i++) {
if (x[i] === undefined) return -1;
if (y[i] === undefined) return 1;
if (x[i] !== y[i]) return x[i] < y[i] ? -1 : 1;
}
}
return 0;
}
const v = ['1.0.0-alpha', '1.0.0-alpha.1', '1.0.0-beta.2', '1.0.0-rc.1', '1.0.0', '1.0.1', '1.2.0', '2.0.0'];
console.log(v.sort(cmp).join(' < '));
"Compare the first three numbers segment by segment, then apply the rule that a version carrying a prerelease identifier sorts below the plain release, and finally compare identifiers numerically.
Output
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-rc.1 < 1.0.0 < 1.0.1 < 1.2.0 < 2.0.0
Expanding caret and tilde into real bounds
node -e "
function expand(r) {
const m = r.match(/^([\^~]?)(\d+)\.(\d+)\.(\d+)$/);
const [, op, M, mi] = m;
const MA = +M;
if (op === '^') return MA > 0 ? '>=' + r.slice(1) + ' <' + (MA + 1) + '.0.0' : '>=' + r.slice(1) + ' <' + MA + '.' + (+mi + 1) + '.0';
if (op === '~') return '>=' + r.slice(1) + ' <' + MA + '.' + (+mi + 1) + '.0';
return '=' + r;
}
for (const r of ['^1.2.3', '~1.2.3', '^0.2.3', '^0.0.3']) console.log(r.padEnd(8), '==>', expand(r));
"With a non-zero major, the caret only blocks the next major; with major 0 it blocks the next minor instead, which is exactly why upgrades are risky during the 0.x phase.
Output
^1.2.3 ==> >=1.2.3 <2.0.0 ~1.2.3 ==> >=1.2.3 <1.3.0 ^0.2.3 ==> >=0.2.3 <0.3.0 ^0.0.3 ==> >=0.0.3 <0.1.0
Confirming build metadata stays out of precedence
node -e "
function cmp(a, b) {
const pa = a.includes('-'), pb = b.includes('-');
const A = a.split(/[.+-]/), B = b.split(/[.+-]/);
for (let i = 0; i < 3; i++) { const d = +A[i] - +B[i]; if (d) return d; }
if (pa !== pb) return pa ? -1 : 1;
return 0;
}
console.log(cmp('1.0.0+build.1', '1.0.0+build.2'), cmp('1.0.0', '1.0.0'));
"Both results are zero: versions with differing build identifiers and the plain version all share identical precedence.
Output
0 0
Deriving the next version from the commit prefix
node -e "
const R = [['fix: x', '1.2.3'], ['feat: x', '1.2.3'], ['BREAKING CHANGE: x', '1.2.3']];
for (const [msg, v] of R) {
const [M, m, p] = v.split('.').map(Number);
const next = msg.startsWith('BREAKING') ? (M + 1) + '.0.0' : msg.startsWith('feat') ? M + '.' + (m + 1) + '.0' : M + '.' + m + '.' + (p + 1);
console.log(msg, '->', next);
}
"Three commit messages all starting from 1.2.3 land on PATCH, MINOR and MAJOR respectively, matching the conventional-commits convention.
Output
fix: x -> 1.2.4 feat: x -> 1.3.0 BREAKING CHANGE: x -> 2.0.0
Common Pitfalls
- Prereleases are not installed by default: ^1.0.0 resolves to a stable release, and getting alpha or rc requires spelling the identifier into the range.
- Build metadata is excluded from precedence, yet npm still treats differing build identifiers as distinct artifacts, so mixing them triggers duplicate-dependency conflicts.
- Writing a dependency as * or omitting the version hands resolution to upstream; the day they ship a breaking release, CI turns red on its own.
- During the 0.x phase the caret blocks the next minor, so moving from 0.2.x to 0.3.0 already carries a breaking change — major-version intuition does not apply.
- A breaking change must increment the major version even when it is a one-line edit; disguising it as MINOR breaks downstream on the next automatic upgrade.
Tips
- ^1.2.3 equals >=1.2.3 <2.0.0 — npm install default behavior.
- ~1.2.3 equals >=1.2.3 <1.3.0 — only patch updates allowed.
- package-lock.json locks exact versions, avoiding uncertainty from ^ and ~.
- Breaking changes must bump the major version, even for tiny changes.
- At major version 0 (0.x.x), any breaking change should bump the minor version.
FAQ
Why do prereleases such as 1.0.0-alpha not satisfy ordinary ranges?
The specification isolates them. A range like ^1.0.0 matches stable releases only, and although 1.0.0-alpha sorts below 1.0.0 numerically it does not satisfy the range unless you spell the prerelease identifier out, as in ^1.0.0-alpha. The rule exists so npm install cannot land on a test build by accident: ^1.0.0 means the stable channel, and only an explicit expression buys you alpha, beta or rc.
If build metadata does not affect precedence, what is it for?
It is a label for humans. Everything after the + sign is excluded from comparison: 1.0.0+build.1 and 1.0.0+build.2 have identical precedence and count as the same version. Its purpose is recording build time, commit hash or packaging environment. The one trap is npm, which still treats two differing build identifiers as distinct artifacts and can end up complaining about duplicate dependencies.
How should version numbers work while the major version is 0?
A major of 0 signals that the public API is not stable yet, so MINOR carries the breaking changes. Expanding ^0.2.3 yields >=0.2.3 <0.3.0: bumping the minor version locks out existing consumers, and that is deliberate protection. During the 0.x phase treat MINOR as the next incompatible generation and reserve PATCH for safe fixes, then publish 1.0.0 once the API is stable and hand the compatibility promise to consumers.
Why does a * dependency suddenly break CI?
Ranges are resolved continuously, not locked once. A * means each install picks whatever is newest at that moment, so the day upstream ships 2.0.0 your source is unchanged while the resolution is not. A lockfile pins exact versions and turns resolution into a one-time decision: libraries should ship loose ranges so downstream users keep freedom, while applications must commit a lockfile so the result is fixed.
What ranges do the wildcard forms 1.x and 1.2.x stand for?
1.x equals >=1.0.0 <2.0.0, opening the entire major version; 1.2.x equals >=1.2.0 <1.3.0, opening only the patch level inside that minor. A wildcard simply marks one segment as any value, so in 1.x that segment is ignored entirely, including any patch number already present there. Wildcards are convenient but hurt reproducibility, so projects relying on them should still commit a lockfile.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us