<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/semantic-versioning-guide-breaking-changes-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/semantic-versioning-guide-breaking-changes-2026/raw.md -->
<!-- Source path: content/guides/semantic-versioning-guide-breaking-changes-2026.mdx -->

---
og_image: "/images/guides/semantic-versioning-guide-breaking-changes-2026.webp"
title: "Semantic Versioning: Breaking Changes Guide 2026"
description: "Complete guide to semantic versioning in 2026: SemVer rules, breaking changes management, conventional commits automation, npm version ranges, and CI."
date: "2026-03-29"
author: "PkgPulse Team"
tags: ["semver", "versioning", "npm", "conventional-commits", "changesets", "2026"]
---

## 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 type | Version bump |
|-------------|-------------|
| `fix:` | PATCH |
| `feat:` | MINOR |
| `feat!:` or `BREAKING CHANGE:` footer | MAJOR |
| `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.

```bash
npx semantic-release
```

Configuration in `release.config.js`:
```javascript
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.

```bash
# 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:

```bash
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](/guides/dependency-management-strategy-monorepo-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:
```javascript
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:

```bash
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:

```bash
# 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](/guides/20-fastest-growing-npm-packages-2026) and [20 npm Packages Losing Downloads Fastest 2026](/guides/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:**
```markdown
## [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](/guides/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
