A little help goes a long way. Find a tool/

Developer FIELD NOTES · 4 MIN READ

How Semantic Versioning Works

MAJOR.MINOR.PATCH as a promise: what each bump means, how prerelease precedence works, and why 1.0.0 is where the contract begins.

A version number is not a number. It is a promise — a compact statement about what changed and whether upgrading will hurt. 1.2.3 looks innocuous, but every digit position and every dash-borne suffix carries a defined meaning, agreed across nearly every package ecosystem. The agreement is called Semantic Versioning, and reading it correctly is the difference between upgrading with confidence and holding your breath.

The problem: version numbers used to lie

Before semver, version numbers were vibes. A project might bump the number when it felt like enough had changed, so “2.1” might be a typo fix or a full redesign, and no automation could tell the difference. In 2011 Tom Preston-Werner formalized the counter-proposal: structure the number so that the riskiest fact — a breaking change — is legible at a glance. The promise comes in three clauses: incompatible API changes bump the major, backwards-compatible new features bump the minor, backwards-compatible bug fixes bump the patch. Everything else is decoration; those three clauses are the contract.

MAJOR.MINOR.PATCH

The core is three non-negative integers separated by dots, each increasing without leading zeros: 1.2.3, 2.0.0, 0.7.14. The zero major is special — 0.x.y signals “anything may break, this is development”, which is why so many young packages live there. Note that the components compare numerically, not as text: 1.10.0 comes after 1.9.9, and a string sort that puts 1.10.0 before 1.2.0 has quietly broken your upgrade script. The three positions are independent: 1.2.3 → 1.2.4 fixes a bug, → 1.3.0 adds a feature, → 2.0.0 takes something away.

Those independent positions are what make automation possible. A dependency manager can safely take 1.2.4 over 1.2.3 without reading release notes, because the patch position carries no risk; it can take 1.3.0 with more confidence than 2.0.0, because the contract says minor bumps do not break. Human release notes still matter for narrative — what the feature does, why the break was necessary — but the machine-readable number answers the first question mechanically: how carefully should I read this changelog?

Prereleases and build metadata

Two optional suffixes refine the picture. A hyphen introduces prerelease identifiers: 2.0.0-beta.1 is a pre-release of 2.0.0, an invitation to test before the breaking changes land. Identifiers are dot-separated words of letters, digits and hyphens — alpha, beta, rc.1, 20260101. A plus introduces build metadata: 1.2.3+build.1847 records how that exact artifact was produced. Metadata has no effect on precedence at all — it exists for humans and CI logs, not for ordering. The grammar has teeth, too: numeric prerelease identifiers cannot carry leading zeros, so 1.2.3-beta.01 is invalid while 1.2.3-beta.1a is fine.

Precedence: who wins

When two versions compete, the rules are mechanical. Compare major, then minor, then patch, numerically; the first difference decides. If all three are equal, a version without a prerelease outranks one with it — 1.0.0 beats 1.0.0-rc.1, because the release is the destination and prereleases are the steps toward it. When both have prereleases, compare identifiers left to right: numeric identifiers compare numerically (beta.2 < beta.10), numeric identifiers rank below alphanumeric ones (alpha.1 < alpha.beta), alphanumeric identifiers compare in ASCII order (alpha < beta), and a longer identifier list beats a prefix of itself (alpha < alpha.1). Chained together, these rules produce the canonical ladder: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.

Why 1.0.0 matters

The first stable release is the moment the contract starts. Before it, everything is negotiable; after it, a major bump must mean a documented breaking change. This is why ecosystems treat 1.0.0 as a milestone rather than a starting point, and why “still at 0.x” is an honest warning label. The contract also powers tooling: dependency managers interpret caret and tilde ranges — ^1.2.3 means “any 1.x.y at least 1.2.3” — by trusting that minor and patch bumps are safe. The number is machine-readable policy.

Try it yourself

Test your intuition in the Semver Comparator: compare two versions head to head to see which outranks the other and why, or paste a release list to sort it by true precedence — including prereleases, which string sorting always mangles. Versions usually travel inside package manifests; JSON Explained covers the format they ride in, and naming conventions like those in camelCase vs snake_case vs kebab-case decide how the package names around those versions are written.

Frequently asked questions

Is 1.2 a valid semantic version?
No — semver requires exactly three numeric parts, so the correct spelling is 1.2.0. Strings like 1.2 or 1.2.x belong to range notations, not to version precedence.
Why is 1.0.0-alpha less than 1.0.0?
A prerelease version has lower precedence than the release it belongs to. Prereleases are the steps leading to a release: alpha, beta, rc, then the real 1.0.0.
Does build metadata affect comparison?
No. 1.2.3+build.1847 and 1.2.3 have equal precedence by spec — metadata records how an artifact was built, not which release it is.
What does the leading 0 in 0.x.y mean?
A major version of zero signals development: anything may change without notice. Many ecosystems treat 0.x packages as exempt from the compatibility promises that start at 1.0.0.