Semantic Versioning Explained: When to Bump Major, Minor, or Patch
A practical guide to semantic versioning rules, choosing the right version bump for breaking changes, features, and fixes, plus prerelease tags for npm releases.
Semantic Versioning Explained: When to Bump Major, Minor, or Patch
Every version number you publish is a promise. major.minor.patch is a three-part contract that tells anyone installing your package what kind of change they are about to pull in: a safe fix, a new feature they can ignore, or a breaking change that needs their attention. Get the bump wrong and you either scare users away with phantom major releases or, worse, slip a breaking change into a patch that crashes a hundred build pipelines overnight.
This guide covers the rules of semantic versioning, the one question that decides every bump, how prerelease tags work, and a worked example you can follow step by step. If you want the answer without reading the rules every time, the Semver Increment tool shows all the bumps for any version at once.
The Three Numbers and What Each One Promises
A semantic version looks like 1.2.3. Each segment carries a distinct meaning:
- Major (
1.x.x) — incompatible API changes. Bumping this says: do not upgrade blind. - Minor (x.
2.x) — new, backward-compatible functionality. Existing code keeps working. - Patch (x.x.
3) — backward-compatible bug fixes. Nothing new, nothing removed.
The rule that trips people up is the reset. When you bump a higher segment, every lower segment goes back to zero. A minor bump on 1.2.3 gives 1.3.0, not 1.3.3. A major bump on 1.2.3 gives 2.0.0, not 2.2.3. The reset is part of the signal: 2.0.0 reads as a clean break, and trailing leftovers from the old line would only confuse it.
One more thing the segments are not: they are not decimals. 1.2.9 followed by a patch bump becomes 1.2.10, never 1.2.91. Each part is an integer that carries the way a counter should. I have seen sort scripts and changelog generators choke on this exact assumption, treating 1.2.10 as older than 1.2.9 because someone compared the strings instead of the numbers.
The One Question That Decides Every Bump
Before you touch a version number, ask: what did my change do to the people who depend on this code?
That single question resolves almost every case:
- It breaks something they relied on → bump major. Removed a function, renamed a config key, changed a default, dropped a Node version — anything that can make working code stop working. Breaking changes always bump major, even if the diff is one line.
- It adds something new without breaking the old → bump minor. A new option, a new exported helper, a new flag with a safe default.
- It fixes a bug without changing the surface → bump patch. The behavior people already use simply works better now.
So a bug fix bumps the patch (1.4.2 to 1.4.3), and a breaking change bumps the major (1.4.2 to 2.0.0). The size of the code change is irrelevant. A one-character fix that removes a public method is still a major bump, because the contract changed. Matching the bump to the consumer impact rather than the line count is the entire discipline.
A Worked Example: From 1.2.3 to 1.3.0 to 2.0.0
Walk a single library through a few releases so the resets click.
Start at 1.2.3. You ship a backward-compatible bug fix to date parsing. Nothing in the public API moved, so you bump the patch: 1.2.4.
Next release, you add a new optional timezone argument. Old calls still work because it defaults sensibly. That is a feature, so you bump the minor and reset the patch to zero: 1.3.0. Note the patch did not stay at 4 — the minor bump wiped it.
Later you decide the old positional-argument signature has to go in favor of an options object. Existing callers will break. That is unambiguously a breaking change, so you bump the major and reset both lower segments: 2.0.0. Not 2.3.0, not 2.0.4 — a major bump clears everything below it.
Three releases, three different bumps, one rule each time. If you ever want to confirm which of two versions is actually higher after a chain like this, Semver Compare sorts them by the real precedence rules instead of string order.
Prerelease Tags: Shipping a Release Candidate
Sometimes a release is risky enough that you want testers on it before it goes stable. That is what prerelease tags are for. They hang off the version with a hyphen: 2.0.0-beta.0, 2.0.0-rc.1.
The npm-style increments give you four prerelease verbs:
- premajor — first prerelease of the next major.
1.2.3premajor with identifierbeta→2.0.0-beta.0. - preminor — first prerelease of the next minor.
1.2.3→1.3.0-beta.0. - prepatch — first prerelease of the next patch.
1.2.3→1.2.4-beta.0. - prerelease — advances an existing prerelease counter.
2.0.0-beta.0→2.0.0-beta.1.
The preid (prerelease identifier) is the channel name: beta, alpha, rc, next. Leave it empty and you get a bare numeric tag like 2.0.0-0. Two behaviors are worth memorizing. First, prerelease precedence is lower than the stable version it points at — 2.0.0-beta.1 comes before 2.0.0, which is why testers on the beta get superseded the moment you publish the real 2.0.0. Second, switching the identifier resets the counter: 1.2.3-beta.3 advanced with rc restarts at 1.2.3-rc.0.
And the rule people forget most often: a stable bump resolves a prerelease down, it does not skip ahead. A patch bump on 1.2.4-0 lands on 1.2.4, not 1.2.5, because the -0 prerelease was already leading up to that very patch.
How This Plays Out in npm and Real Releases
In an npm project, package.json holds the version and npm version major|minor|patch (or the prerelease verbs) does the bump, writes the file, and creates a git tag in one shot. The numbers you pick flow straight into what consumers see in ^1.2.3 and ~1.2.3 ranges: a caret allows minor and patch upgrades, a tilde allows only patch. That is precisely why an honest bump matters — a user who pinned ^1.0.0 expects every later install to keep working, and a sneaky breaking change inside a minor release is the one that pages someone at 2 a.m.
The first time I leaned on a side-by-side bump table instead of doing the arithmetic in my head, I was tagging a release at the end of a long day. The script asked for the next version, I had 1.9.9 in front of me, and my tired brain wanted to type 1.9.10 — which would have been fine — but I almost reached for 2.0.0 out of pure pattern-matching on the nines. Reading the rows side by side stopped me cold: the change was a bug fix, so patch was the only correct answer, 1.9.10. No breaking change, no feature, no reason to burn a major. Seeing every option laid out turned a judgment call back into a lookup.
That is the workflow the Semver Increment tool is built for: paste your current version, optionally set a preid, and read every bump — stable and prerelease — at once, so you tag the right number the first time.
The Takeaway
Semantic versioning is one question applied consistently: what did this change do to the people depending on it? Breaking changes bump major and reset the rest to zero. Backward-compatible features bump minor. Bug fixes bump patch. Prerelease tags let you cut a candidate first, and stable bumps resolve those tags down rather than skipping past them. Follow the contract and your version numbers do the explaining for you.
Made by Toolora · Updated 2026-06-13