Skip to main content

Semantic Versioning: Breaking Changes Guide 2026

·PkgPulse Team
0

TL;DR

Semantic versioning (SemVer) is a three-number contract: MAJOR.MINOR.PATCH. A major bump means something broke. A minor bump means you added something compatible. A patch bump fixes something. In 2026, conventional commits automate the decision — tools like semantic-release and Changesets analyze commit messages and bump the right number automatically.

Key Takeaways

  • MAJOR (2.0.0): Any breaking change — removed function, changed signature, renamed export, different behavior
  • MINOR (1.2.0): New backwards-compatible features — new functions, new optional parameters, new exports
  • PATCH (1.1.3): Bug fixes that don't change the observable API surface
  • ^1.2.0 in package.json accepts any 1.x version ≥ 1.2.0 — the most common range specifier
  • ~1.2.3 accepts 1.2.x only — useful when minor versions introduce breaking behavior
  • Pre-release versions (1.0.0-beta.1) are excluded from ^ and ~ ranges by default

The SemVer Contract

When your package is at version 1.4.2 and a consumer installs it with "^1.4.2", they're accepting your promise:

  • Any 1.x.y version where x ≥ 4 and y ≥ 2 is safe to install
  • Nothing they depend on will break when upgrading within that range

Breaking that contract destroys trust. A developer who hits a bug after npm update will downgrade and pin — and they'll be annoyed.

The problem is that "breaking change" isn't always obvious. Here's the definitive list:

Always a breaking change (requires MAJOR bump):

  • Removing a function, class, or constant from the public API
  • Renaming an exported symbol
  • Changing a function signature (adding required parameters, changing parameter types)
  • Changing the return type of a function
  • Changing the behavior of an existing function in a non-backwards-compatible way
  • Changing the minimum Node.js version requirement
  • Dropping support for a previously supported platform

Never a breaking change (PATCH is fine):

  • Fixing a bug where the old behavior was clearly wrong
  • Improving performance without changing the interface
  • Updating documentation
  • Adding optional parameters to a function (with reasonable defaults)
  • Adding new properties to a returned object (if consumers don't use destructuring)

Sometimes a breaking change (requires judgment):

  • Adding required parameters to an existing function: MAJOR
  • Narrowing the accepted types of a parameter: MAJOR
  • Widening the return type of a function: may break TypeScript consumers — MAJOR to be safe
  • Removing deprecated functionality: MAJOR even if you warned about it

npm Version Range Specifiers

Understanding how consumers specify your package version determines how your SemVer decisions affect them.

^1.2.3 (caret — most common): Allows changes that do not modify the left-most non-zero digit:

  • ^1.2.3 → ≥1.2.3 <2.0.0
  • ^0.2.3 → ≥0.2.3 <0.3.0 (0.x packages: minor is breaking!)
  • ^0.0.3 → =0.0.3 exactly (0.0.x: every version is potentially breaking)

~1.2.3 (tilde — conservative): Allows patch-level changes only:

  • ~1.2.3 → ≥1.2.3 <1.3.0

1.2.3 (exact): No updates. Risky — you miss security patches.

* (wildcard): Any version. Never use in production. Acceptable only in dev dependencies of applications.

The 0.x trap: Many packages use 0.x versions during development. Under caret semantics, ^0.2.0 only allows 0.2.x updates — consumers can't get 0.3.0 without updating their range. If you want consumers to track minor versions during 0.x, use ~0.2.0 in their package.json.


Conventional Commits: Automating Version Bumps

Conventional Commits is a specification for commit message formatting that enables automated version decisions:

<type>(<scope>): <description>

[optional body]

[optional footer(s)]

Type → version bump mapping:

Commit typeVersion bump
fix:PATCH
feat:MINOR
feat!: or BREAKING CHANGE: footerMAJOR
refactor:, docs:, chore:No version bump

Examples:

fix: handle null values in parsePackage

feat: add support for ESM exports in package resolution

feat!: rename parsePackage to resolvePackage

feat: add registry timeout option

BREAKING CHANGE: default timeout changed from 30s to 10s

The ! suffix or BREAKING CHANGE: footer in the commit body triggers a major version bump.


Automation Tools

semantic-release

semantic-release reads your commit history, determines the next version based on conventional commits, generates a changelog, creates a GitHub release, and publishes to npm — all automatically.

npx semantic-release

Configuration in release.config.js:

export default {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/changelog',
    '@semantic-release/npm',
    '@semantic-release/github',
  ],
}

On every merge to main, semantic-release analyzes commits since the last tag and publishes a new version if any releasable commits exist.

Changesets

Changesets takes a different approach: developers explicitly declare what changed, rather than inferring from commit messages.

# When making a change, create a changeset
npx changeset

# Prompts:
# Which packages were changed? (select from list)
# Is it a major/minor/patch change?
# Describe the change:

This creates a .changeset/random-name.md file committed alongside the changes. At release time:

npx changeset version  # Bumps versions based on accumulated changesets
npx changeset publish  # Publishes and tags

Changesets is better for monorepos (Changesets understands which packages changed and their interdependencies) and for teams that want more control over versioning decisions. For monorepo publishing workflow details, see Dependency Management Strategy for Monorepos 2026.


Managing Breaking Changes Well

When you must make a breaking change, do it right:

1. Deprecate before removing. Add a deprecation notice before removing:

function oldName() {
  console.warn('[DeprecationWarning] oldName() is deprecated, use newName() instead')
  return newName()
}

2. Provide a migration guide. A major version bump without a migration guide is hostile to your users. Write:

  • What changed and why
  • Step-by-step migration instructions
  • Code examples showing before and after

3. Use the major version in your migration timeline.

  • Publish 1.x.0-deprecation-warning for 2+ months
  • Publish 2.0.0 with removal + migration guide
  • Keep 1.x on an LTS branch for security patches for 6-12 months

4. Version the docs. When you release 2.0.0, your docs should have a version switcher. Users on 1.x shouldn't be confused by 2.x docs.


Pre-Release Versions

For alpha/beta versions before a stable release:

npm version 1.0.0-alpha.1
npm publish --tag alpha

npm version 1.0.0-beta.1
npm publish --tag beta

npm version 1.0.0-rc.1
npm publish --tag next

# Final stable
npm version 1.0.0
npm publish  # goes to 'latest' tag by default

Pre-release versions are excluded from ^ and ~ ranges. A consumer on "^1.0.0" won't accidentally get 2.0.0-beta.1. They must explicitly install my-package@beta to get pre-release versions.


Version Auditing: What Consumers Actually Have

Understanding what versions your consumers are running helps you make good deprecation decisions:

# See version stats on npmjs.com
# https://npmjs.com/package/your-package?activeTab=versions

# Check download counts per version via npm API
curl https://api.npmjs.org/versions/your-package/last-week

The distribution matters. If 70% of weekly downloads are on v1.x, don't end-of-life it abruptly. Give a 6-month runway.

For tracking how packages grow or decline in the npm ecosystem, see 20 Fastest-Growing npm Packages in 2026 and 20 npm Packages Losing Downloads Fastest 2026.


The Special Case: 0.x Versions

If your package is at 0.x, you're in a grey zone. The SemVer spec says 0.y.z is for initial development and anything may change at any time. In practice:

  • Many packages stay at 0.x for years (Vite was 0.x for months, now at 6.x)
  • Consumers understand that 0.x means unstable
  • Use 0.y for breaking changes and 0.0.z for patches

When you're ready for stability, jump to 1.0.0. This is a commitment: from here, breaking changes require MAJOR bumps.


SemVer in Practice: Common Scenarios

Scenario 1: Changing Default Behavior

Your function defaults to case-insensitive matching. You change it to case-sensitive (more correct for most use cases).

Is this a breaking change? Yes. Existing code relying on case-insensitive behavior silently behaves differently. MAJOR bump.

Best approach: First, add a caseSensitive option (default false = current behavior). Deprecate the case-insensitive default. In the next major, flip the default to true.

Scenario 2: Adding a Required Parameter

You have doThing(input). You want to add a required context parameter.

Is this a breaking change? Yes. Existing calls missing context break. MAJOR bump.

Best approach: Make it optional with a fallback: doThing(input, context = defaultContext). MINOR bump. Only require it in the next major version.

Scenario 3: Narrowing Accepted Types

Function accepts string | number. You want to change it to only accept string.

MAJOR bump required. TypeScript consumers passing numbers get type errors; runtime consumers get different behavior. No workaround — this is a genuine API surface reduction.

Scenario 4: New Optional Export

You add a new exported TypeScript interface that didn't exist before.

MINOR bump — new exports are additive and backwards compatible.


Communicating Changes Clearly

Version numbers signal "something changed." Changelogs tell consumers what.

Good changelog entry:

## [2.0.0] - 2026-03-29

### Breaking Changes
- `parseConfig` now throws `ConfigError` instead of returning `null` for invalid configs
  - Migration: wrap in try/catch or use the new `parseConfigSafe()` for null-return behavior

### Added
- `parseConfigSafe()` backwards-compatible wrapper
- `ConfigError` class with detailed error messages

The good changelog tells consumers exactly what changed and how to migrate. A bad changelog entry like - Updated parseConfig forces users to read the diff.

For tracking which npm packages gain or lose adoption — an indicator of whether your versioning strategy is working — see 20 npm Packages Losing Downloads Fastest 2026 for patterns in why packages fall out of favor.


Versioning at Scale: Multi-Package Repositories

When a single repository contains multiple packages — a monorepo — SemVer becomes more complex. Changing a shared utility library might require version bumps in 10 downstream packages. Doing this manually is error-prone.

The Changesets approach for monorepos:

Changesets solves coordinated versioning by tracking which packages changed and automatically bumping dependents. When you make a change:

  1. Run npx changeset to describe the change and its type (major/minor/patch)
  2. Changesets records this intent in a .changeset/ file
  3. When you're ready to release, npx changeset version reads all accumulated changesets, bumps each package appropriately, and updates their CHANGELOG.md
  4. Downstream packages that depend on a bumped package also get minor bumps automatically

This ensures the version bump cascade is correct and documented — you don't accidentally release a library change without bumping the apps that consume it.

Independent vs. fixed versioning:

Changesets supports two versioning strategies:

  • Independent (default): each package has its own version, bumped based on what changed in that package
  • Fixed: all packages in the monorepo share the same version (bump anything → bump everything)

Most monorepos benefit from independent versioning — packages evolve at different rates. Fixed versioning makes sense for tightly coupled packages that are always released together (like a design system with component packages and a theme package).


The Lifecycle of a Major Version

A major version isn't just a number change — it's a commitment to a support timeline.

Before release:

  • Write the migration guide before you write the breaking code. Understanding the migration path first helps you design better APIs.
  • Add deprecation warnings in the last minor version of the previous major (e.g., the 1.9.0 release should warn about APIs removing in 2.0.0).

At release:

  • Publish the migration guide prominently — GitHub release notes, README, docs site
  • Tag the old major's release branch (e.g., v1) for continued maintenance
  • Announce through your community channels (GitHub Discussions, Discord, npm page)

After release:

  • Maintain the previous major for at least 6 months with security patches
  • Set a clear end-of-life date — consuming teams need runway to migrate
  • Monitor GitHub Issues for migration blockers you didn't anticipate

The teams whose users love their upgrade experiences are the ones who treat a major version as a collaborative transition, not a unilateral change.


Methodology

This article draws on:

  • Semantic Versioning 2.0.0 specification (semver.org) — canonical authority
  • Conventional Commits 1.0.0 specification (conventionalcommits.org)
  • semantic-release documentation and configuration guides
  • Changesets documentation
  • npm documentation on version ranges and pre-release tags
  • WorkOS blog on software versioning practices
  • IEEE research on semantic versioning and breaking changes

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.