🔖 Semantic Version Bumper
Parse, bump, and range-test SemVer strings — no install required
Caret vs Tilde vs Greater-Than-Equal: How SemVer Ranges Actually Work in Your package.json
Every JavaScript developer has pasted a package.json and skimmed past the little punctuation symbols before version numbers — the ^, the ~, the >= — without fully thinking about what they permit. Most of the time this works out fine. But when it doesn't, the resulting bug is famously confusing: your CI pipeline passes on Monday, breaks on Thursday, and nobody changed your code. The culprit is almost always a transitive dependency that released a new version that your range specification silently permitted.
Understanding Semantic Versioning (SemVer) range operators is therefore not just academic. It directly affects build reproducibility, security patch uptake, and the sanity of your on-call rotations.
The SemVer Contract: What the Three Numbers Mean
SemVer defines a version as MAJOR.MINOR.PATCH. The meaning of each segment comes with a social contract between library authors and consumers:
- PATCH (e.g., 1.4.2 → 1.4.3): Bug fixes only. No new API surface. Safe to take automatically.
- MINOR (e.g., 1.4.2 → 1.5.0): New features, but fully backwards-compatible. Existing call sites should not break.
- MAJOR (e.g., 1.4.2 → 2.0.0): Breaking changes. Consumers may need to update their code.
The prerelease suffix (-alpha.1, -beta.3, -rc.2) signals versions that are not yet stable. By convention, a prerelease version has lower precedence than the release it precedes — so 1.5.0-beta.1 < 1.5.0.
The Caret (^): Permissive Within a Major
The caret is npm's default range operator when you npm install a package. ^1.4.2 means: any version that is >= 1.4.2 and < 2.0.0. So it will happily pull in 1.4.9, 1.5.0, or even 1.99.0, but will never jump to 2.0.0.
The rationale is that a well-maintained library should not introduce breaking changes within the same major version. If it does, that's considered a violation of the SemVer contract — and the library author, not you, is to blame. In practice, small or solo-maintained packages sometimes slip breaking changes into minor releases, which is exactly why pinning matters more for critical dependencies.
One edge case: when the major version is 0 (i.e., ^0.4.2), the caret tightens significantly — it becomes equivalent to ~0.4.2, restricting updates to patch-level only. This is because 0.x versions are explicitly "initial development" under the SemVer spec and carry no backward compatibility promise.
The Tilde (~): Conservative, Minor-Locked
The tilde operator is stricter. ~1.4.2 means: any version that is >= 1.4.2 and < 1.5.0. Only patch bumps are accepted; a minor bump will fail the range check.
Teams that operate in regulated environments, or maintain libraries with extremely stable APIs, often prefer tilde ranges for production dependencies. The trade-off is that you get security patches automatically (patch-level) but you have to explicitly opt in to new minor features by updating your pinned range.
The tilde is a good middle ground between the caret's permissiveness and a fully locked exact version. It answers the question: "I trust this minor version of the library; let me just get bug fixes."
Greater-Than-Equal (>=): Wide Open
The >= operator is exactly what it looks like — any version at or above the specified floor, with no upper bound. >=1.4.2 would match 1.4.2, 1.5.0, 2.0.0, even 99.0.0. This is rarely used in package.json for production dependencies because it completely removes the breaking-change guard that major versioning provides.
Where >= does appear legitimately is in peer dependency declarations. A React component library might declare "react": ">=16.8.0" as a peer dependency, signaling: "I work with any reasonably modern React, not just one specific minor line." Here, the intentional openness makes sense because the consuming application controls which React version it installs.
Bump Types in Practice: When to Cut Which Release
Knowing the range operators also helps you decide what kind of release to cut when you're the library author:
Patch bump: You fixed a null pointer exception on an edge case input. No new function signatures. No changed behavior for valid inputs. Cut a patch release. Your consumers with ~ or ^ ranges will pick it up automatically on next install.
Minor bump: You added a new optional parameter to an existing function, with a safe default. Or you added a brand new utility function. Existing callers are unaffected. Cut a minor release. Consumers with ^ ranges will pick it up; those with ~ ranges will not (by design).
Major bump: You renamed a core function, changed a return type, or removed a deprecated feature. Anyone upgrading must audit their code. Cut a major release and write a migration guide. No consumer range will pick this up automatically unless they explicitly widen their range.
Prerelease bump: You want early adopters to test a new feature before it stabilizes. Add a prerelease tag (-beta.1, -rc.2). Standard range operators like ^ and ~ will not resolve prerelease versions unless the consumer explicitly opts in with a prerelease-aware range or by pinning the exact tag. This is a safety feature — your stable-release users are shielded from experimental builds.
Lockfiles and the Illusion of Safety
Here's a subtlety that trips up even experienced developers: if you have a lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml), your installed versions are frozen regardless of your range specification. So ^1.4.2 in package.json will not auto-update to 1.5.0 on the machine that generated the lockfile. But on a fresh checkout or a CI run that regenerates the lockfile, it might.
This is why npm ci (clean install) differs from npm install. The former strictly respects the lockfile; the latter may update it. The range in package.json acts as a boundary for what lockfile regeneration is allowed to pull in — it's not an instruction to update on every install.
The practical upshot: your range operators define the policy; your lockfile defines the current state. Both matter, and both need to be reviewed when evaluating a dependency upgrade.
Testing Your Ranges Before You Commit
The safest workflow when bumping dependencies or publishing a new version is to explicitly verify that the versions you care about satisfy (or don't satisfy) the ranges you expect. Tools like the Semantic Version Bumper above let you do this interactively — enter your base version, enter the candidate target, and instantly see whether caret, tilde, or gte ranges would match.
This is especially useful when you're deciding whether to publish a change as a patch or minor release. If your consumers mostly use ~ ranges, a minor bump means they must manually update. If they use ^ ranges, a minor bump flows through. Neither is wrong — but knowing which your ecosystem expects helps you communicate changes accurately and reduce surprise upgrades.
SemVer is ultimately a communication protocol between maintainers and consumers. The range operators are the grammar. Getting fluent in both sides of that grammar is one of the small, high-leverage skills that separates a developer who ships stable software from one who's always firefighting mysterious dependency regressions.