skip to content

In Semantic Versioning (SemVer), a library bumps its published version from 2.3.5 to 2.4.0, and later from 2.4.0 to 3.0.0. What does each of those two jumps tell a consumer about what changed, and why does that difference matter when deciding whether to upgrade?

level: juniorimportance: must knowfreq 80%

answer

  1. MAJOR.MINOR.PATCH = breaking.feature.fix
  2. higher position resets lower positions to zero
  3. it's a promise, not enforced
  4. Hyrum's Law - undocumented behavior becomes API
  5. caret range trusts the MAJOR boundary

basics

~10 s

SemVer is MAJOR.MINOR.PATCH. 2.3.5→2.4.0 (MINOR) means new features were added, safe to upgrade. 2.4.0→3.0.0 (MAJOR) means something incompatible changed - your code might break, so check the changelog first.

solid answer

~40 s

SemVer encodes three promises in one number: PATCH bumps for backward-compatible bug fixes, MINOR bumps for backward-compatible new functionality, and MAJOR bumps for changes that break the public API. 2.3.5→2.4.0 is a MINOR bump - the maintainer is promising nothing that depended on 2.3.x should break. 2.4.0→3.0.0 is a MAJOR bump - it's an explicit signal that some public-facing contract changed (removed function, changed signature, altered default behavior) and consumers must read the changelog and likely change their own code before adopting it. This distinction is what lets tools like npm, Cargo, or Gradle auto-upgrade MINOR/PATCH releases safely while gating MAJOR upgrades behind a human decision.

go deeper

for a junior

Can state the three-part format and correctly say which letter means what; doesn't need to know Hyrum's Law or resolver internals yet.

for a middle

Should connect the number to caret/tilde range behavior in their day-to-day package manager and know that a PATCH bump can still break things in practice.

for a senior

Should articulate SemVer as an unenforced social contract, explain Hyrum's Law risk, and describe how their team's CI still runs full tests on dependency bumps rather than trusting the version alone.

for a principal

Should discuss how to design deprecation/removal policy across an org's own published packages so internal consumers can trust the org's MAJOR/MINOR/PATCH signals, and where automated tooling fits into acceptable risk.

## What the three numbers mean **Semantic Versioning**, formalized at semver.org by Tom Preston-Werner, turns a version string into a machine-and-human-readable promise about compatibility rather than an arbitrary label. The format is `MAJOR.MINOR.PATCH`, and each position has a strict, independent meaning: - Increment **PATCH** when you ship a backward-compatible bug fix. - Increment **MINOR** when you add backward-compatible functionality. - Increment **MAJOR** when you make an incompatible change to the public API. Crucially, when a higher position increments, everything below it resets to zero — so `2.4.0` followed by a fix becomes `2.4.1`, not `2.4.0.1`, and a breaking change from `2.4.1` becomes `3.0.0`, not `2.5.0` or `2.4.2`. ## The question each position answers Mechanistically, the three numbers exist to answer three different questions a consumer's build tool asks before pulling a new version: | Position | The question it settles | |---|---| | PATCH | 'is this safe to take automatically because nothing changed except a bug got fixed?' | | MINOR | 'is this safe to take automatically because only additive capability arrived?' | | MAJOR | 'must a human look at this before it's taken, because something I depend on might now behave differently or not exist?' | This is why package managers structure their default version ranges around this contract: a caret range like `^2.3.5` in npm will happily accept `2.4.0` or `2.9.9` but never `3.0.0`, precisely because the SemVer contract says only the leading non-zero digit protects against breakage. ## Why it exists at all The reason this exists at all is **coordination cost at scale**. Before SemVer became a de facto standard (popularized around 2010-2011 and now the default in npm, Cargo, Go modules, and most language package managers), version numbers were largely marketing artifacts — a vendor could jump from 4.2 to 5.0 for a UI refresh with zero API impact, or silently break callers in a 'minor' 2.1 to 2.2 release. That made automated dependency resolution effectively impossible: a resolver couldn't decide which upgrades were safe without a human reading every changelog. SemVer replaces that ambiguity with an explicit, checkable contract, which is what makes tools like Dependabot, Renovate, `npm update`, or Cargo's default resolver able to bump dependencies unattended for MINOR/PATCH and only open a distinct, higher-risk PR for MAJOR bumps. ## The trade-off The trade-off is that SemVer is a **social contract**, not something the runtime enforces. Nothing stops a maintainer from publishing `1.4.2` that quietly changes a function's return type, or from treating a 'MINOR' release as license to deprecate and immediately remove something. The specification only constrains the *meaning* of the number; compliance depends entirely on the maintainer correctly classifying their own change, which requires knowing the full public API surface, including behavior that was never formally documented but that users came to depend on (**Hyrum's Law**). Teams that treat SemVer as a hard guarantee and blindly auto-merge MINOR/PATCH bumps in CI can get burned by a maintainer's classification mistake, so mature pipelines still run the full test suite against upgraded dependencies rather than trusting the version number alone. ## Failure modes Failure modes show up in a few recognizable shapes in production. 1. The most common is the **'accidental breaking patch'** — a maintainer fixes what they believe is a bug, but some consumers were relying on the old (buggy) behavior, so the PATCH release breaks them; this is functionally a MAJOR change mislabeled as a PATCH. 2. Another is **'MINOR creep'**, where a team ships a new required configuration option or changes a default value under a MINOR bump because it 'adds a feature', when in fact existing callers now behave differently — a classic Hyrum's-Law violation. 3. A third is **transitive breakage**: your direct dependency correctly follows SemVer, but one of *its* transitive dependencies didn't, and an automated MINOR bump three levels down the tree breaks your build even though every intermediate package technically obeyed the spec. ## Where it shows up A concrete real-world illustration is the npm ecosystem's `^` (caret) default: when you run `npm install lodash`, package.json records `^4.17.21`, meaning npm will auto-resolve any 4.x.y >= 4.17.21 but will never silently take 5.0.0. This single convention — built entirely on trusting that lodash's maintainers apply MAJOR bumps correctly — is what allows `npm install` in a fresh CI job to pull slightly newer PATCH/MINOR releases automatically without anyone opening a PR, while a jump to lodash 5 requires an explicit, reviewed change to package.json. The entire automated-upgrade ecosystem is built on the assumption that this contract mostly holds, with test suites as the safety net for the cases where it doesn't.

  • If a maintainer removes a deprecated function in what they publish as a MINOR release, what's actually wrong with that, precisely?
    Removing any part of the public API is an incompatible change by SemVer's definition, regardless of how long it was marked deprecated - deprecation is a warning, not a version-number exemption. It should have been a MAJOR bump; publishing it as MINOR breaks the caret-range contract consumers and their tooling rely on, causing an unreviewed `npm install` to fail or misbehave.
  • Why might a resolver treat a caret range differently for a package still below 1.0.0?
    SemVer explicitly exempts the 0.x.y range from the full compatibility promise, so tooling narrows the caret's safe zone to only PATCH-level bumps for 0.x packages instead of the full MINOR range it allows once a package hits 1.0.0. This mirrors the spec's own statement that 0.x APIs shouldn't be considered stable.
  • How does a team typically isolate whether a test failure came from a package's own breaking change versus one of its transitive dependencies?
    They generally can't tell from version numbers alone - both look like 'tests failed after upgrade.' Practically, teams bisect the lockfile, upgrade one dependency at a time, or read the failing package's own changelog to see if it bumped something below it in the tree.

Think of SemVer like a restaurant's menu-change policy: PATCH is 'same recipe, fixed a typo on the label', MINOR is 'added a new optional topping, your usual order still works', MAJOR is 'we changed the recipe itself - if you cared about the old ingredients, check before you order again.'

saying these in an interview costs you the question

  • Says MAJOR/MINOR/PATCH are just marketing/arbitrary numbers
  • Thinks any version bump higher than PATCH is fine to auto-merge without tests
  • Doesn't know a removed deprecated function is still a breaking change
  • Believes SemVer is enforced by the package registry/tooling itself
  • Can't explain why lower numbers reset to zero on a higher bump

context