What Is Semantic Versioning? How Semver (X.Y.Z) Numbers Work

When you update a package, your team needs a fast answer. Will this update break the build? Semantic versioning exists for exactly that question. It turns a version number into a message. Package managers such as npm, pnpm, and yarn also resolve dependencies through this same convention.
What Is Semantic Versioning?
Semantic versioning is a numbering convention for software packages. It splits a version into three parts and signals the scope of a change in a consistent way. The X.Y.Z pattern stands for major, minor, and patch, in that order. Each number also points to a different kind of change.
Semver.org publishes the official specification. The short word "semver" refers to that same document. The spec ties every version increment to an explicit rule instead of habit or guesswork. As a result, two teams reading the same number reach the same conclusion.
The spec also defines a "public API". Documented functions, data types, and behavior, for instance, fall inside that boundary. Internal code can change freely, as long as that public surface stays the same. In that case, semantic versioning treats the internal change as invisible to the version number.
Teams sometimes debate whether a change counts as minor or patch. The spec gives one simple test. Does existing code that calls the package keep working without edits? If the answer is yes, the change is minor at most. Otherwise, it is major.
How Do You Read an X.Y.Z Version Number?
In the X.Y.Z pattern, X is major, Y is minor, and Z is patch. The order runs left to right by importance. Take version 2.4.1, for example: major is 2, minor is 4, and patch is 1. Together, these three numbers summarize the package's change history.
Each number resets to zero once a number to its left increases. A major bump, for instance, resets both minor and patch. A minor bump resets only patch. Because of this rule, a reader never has to recount past changes to understand the current number.
A developer can often judge a dependency list from the numbers alone. Some packages went through a recent overhaul; others stayed quiet. In practice, this reading skill becomes second nature over time, much like scanning a diff.
- X (major): breaking, backward-incompatible changes.
- Y (minor): backward-compatible new features.
- Z (patch): backward-compatible bug fixes.
When Does the Major Version Increase?
The major version increases whenever a change breaks the public API. The spec states this plainly. If an existing function's behavior, parameters, or return value changes, and that break hits projects running the old code, the change counts as major.
A major bump also works as a warning sign. Before you update, you read the release notes first. You then review your own code for conflicts. For that reason, teams usually schedule a major upgrade for a planned maintenance window instead of pulling it in automatically.
A maintainer often signals a coming break ahead of time. First, the maintainer marks a feature as deprecated in one release. Only later, in a future major release, does the maintainer actually remove it. This two-step approach gives downstream users time to prepare.
A package can change several things at once. Even so, the major number still climbs by only one. Semantic versioning answers one question: what is the biggest kind of change here? A single breaking edit bundled with ten small fixes still forces a major release.
When Does the Minor Version Increase?
The minor version increases when a maintainer adds a backward-compatible feature. Existing code, therefore, keeps running without edits. A new option or function simply becomes available alongside the old ones.
The spec also calls for a minor bump once a maintainer starts deprecating a feature. That step warns users without breaking anything yet. Maintainers can likewise bundle internal improvements into a minor release and announce them in the same notes.
Picture a library that ships a new configuration option in version 2.3.0. A project that ignores that option keeps running exactly as before. Instead, only the team that wants the new behavior reads the docs and opts in.
When Does the Patch Version Increase?
The patch version increases for a backward-compatible bug fix. According to the spec, a bug fix is an internal change that corrects incorrect behavior. The visible API stays the same; only the unexpected behavior disappears instead.
Most teams treat patch releases as the safest kind of update. By definition, a patch adds no new function and changes no existing usage. That is why many teams let patch updates flow through automatically, with a short review at most.
A jump from 1.2.5 to 1.2.6 typically fixes a bug alone. In that case, a team can let the update through without manual review. However, a release labeled "patch" that quietly changes behavior breaks the semver contract.
What Does a 0.y.z Version Mean?
The spec calls the 0.y.z range "initial development". In other words, anything can change at any time during this phase. While a package sits at 0.y.z, its public API has not reached a stable state yet.
During this phase, a team cannot read minor and patch bumps the same way it would after 1.0.0. The maintainer has not made a stability promise yet. For that reason, you track a 0.y.z dependency's release pace closely before you move it into production.
Many new open-source projects stay in 0.y.z on purpose. Instead, they want to gather early feedback first. The maintainer treats every release as a potential breaking change and says so plainly in the docs.
Why Does the Move to 1.0.0 Matter?
Version 1.0.0 marks the point where the public API becomes defined. From that point on, a real stability commitment begins. From that version forward, the spec evaluates every change against the major, minor, or patch rules.
A project that depends on a pre-1.0.0 package should expect behavior changes with each update. After 1.0.0, developers can generally assume otherwise. Updates sharing the same major number should not break the API.
This move also shifts responsibility. The maintainer now commits to announcing every breaking change through a major bump. Without that commitment, users cannot trust the number. They end up testing each update from scratch instead.
What Do Pre-Release Tags (Alpha, Beta, RC) Mean in Semantic Versioning?
A pre-release tag is a set of dot-separated identifiers. You append it right after the patch number with a hyphen, as in 1.0.0-alpha.1. The spec does not require the words alpha, beta, or rc. Those are, in other words, simply common, valid examples.
A pre-release tag tells readers that a release is not stable yet. In practice, the release can still change at any point. Identifiers may only contain ASCII letters, digits, and hyphens. They cannot be empty, and a numeric identifier cannot start with a leading zero.
Many teams follow one common order. Alpha marks the earliest, least stable stage. Beta comes next, once the feature set has settled but testing continues. Finally, rc, short for release candidate, marks the last check before a stable release. The spec itself does not enforce this order.
How Do You Order Pre-Release Numbers?
A pre-release always ranks below the normal version that shares its core number. So 1.0.0-beta always ranks below 1.0.0. When you compare two pre-releases, you compare their identifiers one by one, left to right.
Specifically, numeric identifiers compare numerically. Every other identifier compares in ASCII order instead. A numeric identifier always ranks below one that contains letters. The spec's own example ordering runs as follows: 1.0.0-alpha, then 1.0.0-alpha.1, then 1.0.0-alpha.beta, then 1.0.0-beta, then 1.0.0-beta.2, then 1.0.0-beta.11, then 1.0.0-rc.1, and finally 1.0.0.
This order matters in practice. A package manager relies on it to pick the "latest" pre-release correctly. A range such as ^1.0.0-alpha depends on these same comparison rules.
What Is Build Metadata?
Build metadata is extra information appended to a version. You add it with a plus sign and dot-separated identifiers, as in 1.0.0+20130313144700. This field, for instance, commonly carries a build timestamp or a commit identifier.
The spec tells you to ignore build metadata during a precedence comparison. That is why 1.0.0+a and 1.0.0+b carry equal precedence. The difference lives only in the extra label. Either way, it never changes the version order.
Teams often use build metadata for internal tracking. For instance, it can record which build machine produced a given package. Users can see that label, but it never drives an update decision on its own.
How Do CI/CD Pipelines Use Semantic Versioning?
A release pipeline can decide which part of the version to bump automatically. It scans commit messages or tags before it cuts a new release. Many tools read a "fix:" commit as a patch. A "feat:" commit becomes minor, and any commit mentioning "BREAKING CHANGE" becomes major.
This automation removes some human error from the process. The team no longer picks a number by hand on every release. Even so, the tool only reads commit messages; it does not understand the code itself. The developer's commit message still has to classify the change correctly.
Adding a review step before release helps here. That step catches cases where an automated bump misreads a commit and produces the wrong number. As a result, the team keeps the speed of automation without losing reliability.
How Do Version Ranges Work Inside package.json?
A dependency line in package.json usually names a range, not one fixed version. As a result, the package manager can pick the newest version that still fits. npm's own documentation defines these ranges with symbols such as tilde and caret.
Writing a range once saves the team from editing package.json by hand on every update. Instead, the package manager records the exact installed version in a lock file alongside that range.
Before picking a range, a team usually weighs how reliably a dependency releases. A caret range, for example, tends to work well for a package that follows semver strictly. A rarer release schedule, or a history of broken semver, calls for a narrower range instead.
What Does the Caret (^) Symbol Mean?
npm's docs state that the caret symbol accepts minor-level updates. Writing ^1.0.4 accepts any version in the 1.x.x range. It stops short of 2.0.0. In short, this range assumes the major number will stay fixed.
Many projects pick caret as their default range. After all, semantic versioning promises that updates sharing a major number will not break the API. Caret behavior shifts for 0.y.z packages, though. In that case, you still want to check release notes directly.
Write ^0.4.2, for instance. npm applies a special rule for zero major versions here. It keeps you inside 0.4.x rather than jumping to 0.5.0. A minor number inside 0.y.z still carries no stability promise. That detail catches many teams off guard.
What Does the Tilde (~) Symbol Mean?
npm's docs state that tilde accepts only patch-level updates. It defines a narrower range than caret. Writing ~2.2.0, for example, accepts only versions in the 2.2.x range. Put simply, this choice plays more conservative overall.
Teams reach for tilde when they distrust even minor-level changes in a dependency. This way, automation still pulls in bug fixes. New features, meanwhile, stay out until someone reviews them by hand.
Low-tolerance systems, such as finance or healthcare workloads, often pick tilde by default. There, for instance, an unexpected minor change counts as an unacceptable risk. The team therefore routes every new feature through its own review step.
Some teams chase both speed and safety at once. They mix ranges instead of picking one: tilde for critical dependencies, caret for lower-risk helpers. This mixed strategy tunes risk to each dependency, rather than applying one rule everywhere.
How Does Semantic Versioning Make Dependency Management Easier?
This convention lets a team judge update safety from the version number alone. Every package manager can therefore define a range rule. It can then automate the updates that need no human sign-off.
Once a project has dozens of dependencies, tracking each one by hand stops being practical. Semantic versioning changes that. The team then reserves manual review for major changes and automates the rest with confidence.
Many security scanners, specifically, rely on the same rule. Once a scanner finds a vulnerability, it recommends the lowest patch version that fixes it. As a result, the team closes the gap fast, without jumping a major version.
What Goes Wrong in Projects That Skip Semantic Versioning?
A package that ignores semver rules turns its version number into noise. Users, consequently, end up guessing about every update instead of trusting the number. A release labeled "patch" can quietly break the API, which makes automatic updates risky.
Teams facing this situation usually pin the dependency to one fixed version. They then test every update by hand. That choice removes the automation benefit semantic versioning normally provides, and it adds real maintenance overhead.
Some teams offset this risk another way. They widen their automated test suite so it exercises every critical path on each dependency bump. That approach does not replace semver's guarantee, but it limits the damage.
How Do You Apply Semantic Versioning in a Monorepo?
A repository that holds multiple packages has two options. Each package can carry its own X.Y.Z number, or every package can move together under one shared version. The shared approach keeps cross-package compatibility simple; however, it bumps every package on even a small change.
A workspace-aware package manager handles this differently. It tracks which version range an internal dependency uses through its own workspace protocol. This way, packages inside the monorepo avoid re-downloading each other on every publish.
- Decide early whether each package gets its own version or shares one.
- Point internal dependencies at the workspace protocol, not a pinned version number.
- Collect every package's release notes into one list before you publish.
What Mistakes Do Teams Make With Semantic Versioning?
The most common mistake treats a change as "just a patch" when it actually breaks the API. The team ships it without a minor or major bump. Consequently, that single mistake hits every downstream project that trusts the range.
A second mistake mixes pre-release tags inconsistently. One release uses alpha, the next uses beta, without a clear quality difference between them. Updating release notes on every release prevents most of these mistakes.
A third mistake skips the changelog entirely and judges risk from the number alone. After all, the number only tells you the type of change, not the detail. Reading the changelog instead surfaces the real risk, especially around major releases.
- List exactly what changed in the public API before every release.
- Bump the major version for any breaking change, even a small one.
- Use pre-release tags (alpha, beta, rc) in a consistent order.
| Part | Trigger | Example jump |
|---|---|---|
| Major (X) | Backward-incompatible, breaking change | 1.4.2 → 2.0.0 |
| Minor (Y) | Backward-compatible new feature | 1.4.2 → 1.5.0 |
| Patch (Z) | Backward-compatible bug fix | 1.4.2 → 1.4.3 |
What Tools Check Semantic Versioning Compliance Automatically?
Many development setups can diff a new release against the previous one automatically. The tool flags a breaking change in the public API on its own. It catches a function signature change even when a developer labeled the release "patch" by mistake.
In practice, this check finds small but breaking edits that a human reviewer easily misses. That matters most, above all, in a library with a large API surface, where manual review stops scaling. A team that wires this check into its pipeline stops a bad release before it ever reaches production.
How Do You Pick a Library's First Version Number?
A developer starting a new package usually begins at 0.1.0. This choice, in short, keeps the door open to change, without a stability promise yet. Some maintainers instead launch straight at 1.0.0. That sends a clear "this API will not shift" message from day one.
Whichever choice you make, state it clearly in your docs. Otherwise, a user who picks a range cannot tell whether the package sits in 0.y.z or past 1.0.0. In that case, the user cannot fully rely on semver's usual guarantee.
Many maintainers add a small badge near the top of the README for this exact reason. A developer evaluating the package then sees its stability stage immediately. A new team member, likewise, reaches the same information within seconds.
How Does Semantic Versioning Relate to a Changelog?
A version number tells you the type of a change, not its content. A changelog fills that gap instead, so the number is not the only signal. When a maintainer writes a short note for every release, the version number's meaning becomes concrete.
A project that keeps its changelog current helps users decide fast. They can answer "should I grab this update right now" without staring at the number alone. Combine semantic versioning with a changelog, and a team can manage both automated and human-reviewed updates in one workflow.
Put simply, a good changelog entry answers three questions. What changed? Who does it affect? How do you migrate? An entry that covers all three turns a short version signal into a concrete action plan.
How Does a Lock File Complete Semantic Versioning?
A range in package.json defines which versions count as acceptable. A lock file, however, goes further. It pins the exact version installed from inside that range. Together, therefore, they guarantee that a project runs the same dependency tree everywhere.
Without a lock file, two developers running the same caret range can still diverge. One might get a different patch or minor version on a different day. That small gap matters, because some bugs only show up in one specific version anyway.
Once a team commits its lock file to the repository, the picture then changes. The server and every developer's machine now build the exact same dependency tree. That consistency turns semver's promise into a practical guarantee at deploy time.
Which Related Topics Should You Also Check?
Resolving version ranges correctly also depends on a consistent dev environment. Teams on Windows can start with our guide to what WSL2 is. Teams that prefer containers, meanwhile, can read our VS Code dev container guide. On Apple Silicon, our piece on when you need Rosetta is a related read too.
On the Linux side, our guide to choosing a Linux package mirror covers a related reliability question. For the design principles behind a stable public API, see our SOLID principles guide. For a release calendar across a team, see our project management tools for developers guide instead; both are sibling topics in this series. When ESLint and Prettier configs conflict, we cover how to fix that conflict separately.
Our guide on how pnpm differs from npm and yarn goes deeper into package manager choice. Our npm ERESOLVE error fix guide helps instead when dependency resolution hits a conflict. In custom software projects, our team at custom software development tracks dependency versions against this same convention.
You can read the full specification at semver.org. You can also read npm's own range syntax in npm's official documentation.



