Semantic versioning, or semver, carries a deceptively simple promise: a version number tells you what kind of change happened. Bump the patch, nothing breaks. Bump the minor, you added something. Bump the major, something changed that will require consumer attention. The semver specification defines this contract precisely, yet most teams that claim to follow it regularly violate it in ways that quietly corrode trust between package authors and consumers.
The problem isn't ignorance. Most developers can recite the major.minor.patch structure. The problem is that applying semver correctly in a real codebase requires consistent judgement calls at dozens of points in a release cycle, and the guardrails are almost entirely social rather than technical. A CI/CD pipeline won't stop you publishing a breaking change as a minor bump.
The most common semver mistakes in practice
Breaking changes sneaking into minor releases is the most damaging pattern. It happens because teams disagree, often implicitly, about what counts as breaking. Removing a rarely-used field from a JSON response? That's a breaking change for any consumer who reads it. Changing an error code from a string to an integer? Breaking. Narrowing the accepted input range on a function? Breaking. These feel small. They aren't.
The inverse problem is over-bumping major versions. Some teams treat a major release as a marketing event rather than a technical signal, pushing a v2 or v3 when the actual API surface hasn't changed in any incompatible way. This trains consumers to ignore major version signals, which makes the version number useless as a dependency signal. It's the same failure mode as car alarm fatigue.
Patch releases that add behaviour are a subtler issue. The spec says patch is for backwards-compatible bug fixes only. But "we fixed it so it now returns results the caller didn't expect before" is a behavioural change, not just a fix. If a consumer's test suite depended on the old (buggy) output, your patch broke them. This is worth flagging explicitly in the changelog rather than hiding it under "bug fix."
Pre-1.0 is not a blank cheque
The spec explicitly says that anything below 1.0.0 may change at any time, and major-version zero is for initial development. Teams sometimes use this as permission to stay at 0.x indefinitely, shipping breaking changes silently while consumers accumulate. That behaviour is technically allowed. It's also a trust deficit that accumulates until consumers simply stop upgrading.
If your package has external consumers and you're still at 0.x after six months of active use, you probably owe them a 1.0 with a clear changelog. Hiding behind pre-release status when the package is functionally mature just means your users can't write useful version constraints in their own lockfiles.
Speaking of lockfiles: they are not a substitute for correct versioning. A lockfile can lie to you in ways that only surface during a fresh install or a CI rebuild, particularly when transitive dependencies bring in a semver-violating upstream package your direct lockfile didn't capture.
Deprecation vs removal: the gap most teams skip
Correct semver process for removing something goes: deprecate in a minor release (log a warning, update the docs, give consumers a migration path), then remove in the next major. Most teams skip the deprecation step entirely. They either remove immediately in a minor (a breaking change dressed as a feature) or hold off so long that the major bump is enormous and disorienting.
A two-step deprecation-then-removal cycle is boring. It also makes consuming your library feel safe. Safe is the outcome you're after.
The deprecation period length matters too. A two-week window between deprecation and removal is not a real migration window for teams who release on a monthly or quarterly cadence. Three to six months is a reasonable minimum for anything with external consumers. Internal libraries have more flexibility, but the discipline is worth keeping.
Tooling that actually helps
There are three tools worth knowing. Conventional Commits enforces a commit message format that maps directly to semver bumps: fix: maps to patch, feat: maps to minor, and BREAKING CHANGE: in the footer maps to major. Paired with a changelog generator, this automates the version decision in a way that's auditable and consistent.
Release-please (from Google) and semantic-release are both CI-integrated tools that parse conventional commits and open pull requests with the correct version bump and a generated changelog. Neither is perfect, but both remove the human judgement call at the moment when developers are most likely to under-bump.
The third tool isn't a package: it's a documented versioning policy in your repository. A one-page VERSIONING.md that says what counts as a breaking change in your specific codebase is worth more than any linter. It makes the implicit contract explicit, which is the entire point of versioning in the first place.
Internal libraries and monorepos
The semver discipline question gets sharper in a monorepo, where many teams manage multiple internal packages with interdependencies. If you're using a monorepo structure, coordinate your versioning strategy at the repo level, not package by package. Tools like Changesets handle this explicitly, letting contributors declare the impact of their changes at pull-request time rather than at release time when the context is gone.
Internal-only packages with no external consumers technically don't need semver at all. But teams that treat internal packages as unversioned blobs almost always end up with a messy upgrade path when those packages eventually need to be extracted or shared. Starting with semver discipline is cheap; retrofitting it is expensive.
The changelog is part of the version
A version bump without a useful changelog is half a communication. The version number tells a consumer whether to care; the changelog tells them what to do about it. "Bug fixes and improvements" is not a changelog entry. It's a version bump with a post-it note attached.
Concretely useful entries name the thing that changed, the before and after behaviour, and the migration path if one is needed. They're written for the person upgrading at 4pm on a Friday with a broken build, not for the person who already knows the codebase. That's a different audience, and it requires a different kind of writing.
Teams that get semver right aren't following a checklist. They've built a shared understanding of what their version numbers mean to their consumers, and they treat that contract as worth protecting. The tooling helps. The policy document helps. But the discipline is the thing.

