Articles
Published · October 28, 2026

Semantic Versioning Explained: What Major.Minor.Patch Actually Promises

A version number is a promise about your public API. Here is what each of the three numbers in semantic versioning commits you to, and where the rules get fuzzy.

A semantic version such as 2.4.1 is a promise about a public API, not a measure of how much work went into a release. The first number tells dependents whether an upgrade can break them, the second whether it adds something, the third whether it only fixes something.

That's all the scheme asks of you. It only works if you keep the promise. This guide covers what each number commits you to, how to choose which one to bump, and where the rules get fuzzy.

What each number promises

The Semantic Versioning 2.0.0 spec defines a version as three non-negative integers, MAJOR.MINOR.PATCH, with no leading zeros:

  • MAJOR goes up for incompatible changes to the public API.
  • MINOR goes up for new functionality that stays backward compatible.
  • PATCH goes up for backward-compatible bug fixes, meaning internal changes that correct incorrect behavior.

When a higher number goes up, every number to its right resets to 0:

1.4.7  ->  1.4.8   patch: a bug fix
1.4.7  ->  1.5.0   minor: new feature, patch resets
1.4.7  ->  2.0.0   major: breaking change, minor and patch reset

One more rule matters as much as the three bumps: once a version is released, its contents must not be modified. A mistake in 1.4.7 is fixed in 1.4.8, never by quietly replacing the 1.4.7 package.

The public API comes first

SemVer assumes you have declared a public API, in code or in documentation. The spec says it should be precise and comprehensive. Version 1.0.0 is the release that defines it.

Without that declaration the numbers promise nothing. Nobody can tell what counts as "breaking". For a library, the public API is its exported functions and types. For a command-line tool it is the flags and the output format scripts parse. For a Markdown template or a config format, it is the keys and structure other people's files depend on. Write that list down, even if it is five lines in a README, and the bump decision gets much easier.

Deciding which number to bump

Ask what a dependent will experience when they upgrade, and use the lowest number that is honest:

| Change | Bump | Example | |---|---|---| | Fix a crash, no API change | Patch | 1.4.7 to 1.4.8 | | Add an option, old calls still work | Minor | 1.4.7 to 1.5.0 | | Mark a function as deprecated | Minor | 1.5.0 to 1.6.0 | | Remove or rename a function | Major | 1.6.0 to 2.0.0 |

Deprecation deserves a note. The spec requires a minor bump when you mark public functionality as deprecated, and its FAQ advises keeping at least one minor release that carries the deprecation before you remove the feature in a major release. Users get a warning first, and a break later.

The edge cases need judgment. If a bug fix changes behavior that many people relied on, the FAQ says a major release may communicate the change better than a patch, even though the fix technically qualifies as one. The version number should tell users what matters to them. Good commit messages that say what changed make these calls easier at release time, because the list of changes is already written.

If you break the API in a minor release

It happens to everyone. The spec's answer is to fix the problem and release a new minor version that restores compatibility. Modifying a versioned release is unacceptable. Document the offending version and tell your users.

0.x versions and pre-releases

Major version zero, 0.y.z, is for initial development. In the spec's words, anything may change at any time, and the API should not be considered stable. The FAQ suggests starting at 0.1.0 and raising the minor number with each release. Moving to 1.0.0 is the moment you commit to the promise.

Two optional suffixes cover the rest:

  • A pre-release label follows a hyphen, as in 2.0.0-rc.1. It marks the version as unstable and sorts below the matching normal version, so 1.0.0-alpha comes before 1.0.0.
  • Build metadata follows a plus sign, as in 1.0.0+20261028. It is ignored when versions are compared.

How npm ranges lean on the promise

Tools make the promise actionable. npm's documentation recommends starting a package at 1.0.0 and bumping patch, minor, and major as described above. Dependents then choose how much change they accept in package.json:

  • ^1.0.4 accepts minor and patch updates, so it stays below 2.0.0.
  • ~1.0.4 accepts patch updates only.
  • * or x accepts anything, major releases included.

So "my_dep": "^1.0.0" picks up 1.9.3 automatically but never 2.0.0, and "another_dep": "~2.2.0" takes only patch releases. Every one of those rules assumes the maintainer bumped honestly. A breaking change shipped as a patch breaks every project that used a caret range, and none of them did anything wrong.

Pairing version numbers with a changelog

A number says how big a change is; a changelog says what it was. Keep a Changelog recommends stating whether your project follows Semantic Versioning, and describes an Unreleased section at the top that moves under a version heading and date, such as [1.1.2] - 2024-09-27, when you release. If you keep a CHANGELOG.md, the version headings are where your numbers live. A version badge in your README then shows the latest one at a glance.

Keeping the version file next to your notes with Carets

Version numbers, changelogs, and release notes are plain text, and they get edited in small, frequent passes: a heading in CHANGELOG.md, a version line in a config file, a deprecation note in the README. Carets is a native notes and code editor for iPhone, iPad, and Mac that handles those files directly.

  • Markdown, plain text, and code in one editor, with syntax highlighting for the JSON, YAML, and TOML files that carry your version field.
  • Files, projects, and tags to keep a project's changelog, release checklist, and scratch notes together.
  • Native and fast, so a one-line fix on a train doesn't mean opening a full IDE.

Your files stay plain text, which means they stay portable and work with Git and every other tool in your release process. Download Carets on the App Store for iPhone, iPad, and Mac.

Frequently Asked Questions

Should a new project start at 0.1.0 or 1.0.0?

The SemVer FAQ suggests starting at 0.1.0 and raising the minor number for each release during initial development. npm's documentation recommends starting a package at 1.0.0. Either way, 1.0.0 is the point where you declare a public API and promise to keep it stable.

Does a bug fix ever need a major bump?

By the letter of the spec, a backward-compatible fix is a patch. The spec's FAQ says to use judgment: if the fix changes behavior that many users relied on, a major release may communicate the change better.

What if I break the API in a minor release by accident?

Do not edit the released version. Ship a new minor release that restores compatibility, document the offending version, and tell your users.

What does ^1.2.3 mean in package.json?

It accepts minor and patch updates but not a new major, so anything from 1.2.3 up to, but not including, 2.0.0. A tilde range such as ~1.2.3 accepts patch updates only.