· 4 min read
How to Read a Semver Range Correctly
Manesh Jayawardhana
CIO & Co-founder
A dependency is pinned at ^0.5.2. A colleague says the caret allows minor updates, so 0.6.0 will be installed.
It will not. Below 1.0.0 the caret behaves completely differently, and that special case is the source of most semver confusion.
Caret and tilde
Caret allows changes that do not modify the leftmost non-zero digit.
^1.2.3 accepts >=1.2.3 <2.0.0 — any minor or patch release under version 2.
Tilde allows patch-level changes only.
~1.2.3 accepts >=1.2.3 <1.3.0 — patch releases within 1.2.x.
The intent is that caret trusts a package to follow semantic versioning and not break within a major version, while tilde is more conservative and takes only bug fixes.
The 0.x special case
Below 1.0.0, the caret’s rule about the leftmost non-zero digit produces different behaviour.
For ^0.5.2, the leftmost non-zero digit is the minor version. So the caret allows only >=0.5.2 <0.6.0 — patch releases within 0.5.x, which is what a tilde does at 1.x.
For ^0.0.3, the leftmost non-zero digit is the patch. The caret accepts only 0.0.3 exactly.
| Range | Accepts |
|---|---|
^1.2.3 | >=1.2.3 <2.0.0 |
~1.2.3 | >=1.2.3 <1.3.0 |
^0.5.2 | >=0.5.2 <0.6.0 |
^0.0.3 | 0.0.3 only |
The reasoning is that 0.x versions are explicitly unstable under semantic versioning, so a minor bump may break things. Treating it conservatively is the safe interpretation, and it is why a 0.x dependency does not update the way people expect.
Prereleases are excluded by default
^1.2.3 does not match 1.9.0-beta.1, and this surprises everyone the first time.
The rule is that a version with a prerelease tag is only matched by a range that itself mentions a prerelease for that same version tuple. >=1.9.0-0 would match; a plain caret would not.
The reasoning is sound — you do not want a prerelease installed accidentally by a range intended for stable versions — and it means teams publishing prereleases need explicit ranges to consume them.
It also means a package whose only recent releases are prereleases appears to have no updates available under a normal range, which occasionally causes confusion about whether a project is maintained.
Ranges are a trust decision
Choosing between exact pins, tilde and caret is really a choice about how much you trust upstream packages to version correctly.
Exact pins are fully reproducible and mean you receive no security patches without a deliberate update. Combined with a lockfile and a bot that proposes updates, this is a defensible position.
Caret assumes upstream follows semantic versioning honestly. Many packages do; some do not, and a breaking change shipped in a minor release is a recurring source of broken builds.
Lockfiles make this less critical than it used to be. With a lockfile committed, the range only matters when the lock is regenerated, and installs are reproducible in between.
The practical position most teams reach: caret ranges plus a committed lockfile plus automated update proposals reviewed by a human.
Lockfiles decide what actually installs
Ranges describe what is permitted. Lockfiles record what was chosen.
With a lockfile committed, every install reproduces the exact versions recorded, regardless of what the ranges would allow. The range only comes into play when the lock is regenerated — an explicit update, or adding a dependency.
That makes the range a policy about updates rather than a description of your current state. A caret range with a committed lockfile is safe; the same range without one means two developers can be running different code from identical manifests.
The corollary is that a lockfile must be committed for an application. For a published library it usually is not, because consumers resolve their own tree and a library’s lockfile has no effect on them.
Common mistakes to avoid
- Assuming caret behaves the same above and below 1.0.0.
- Expecting prereleases to match a normal range.
- Using ranges without committing a lockfile, so two installs differ.
- Pinning everything exactly and never updating, which accumulates security debt.
- Trusting that a minor version bump from every package is non-breaking.
How to do it with Semver Range Tester
The Semver Range Tester shows the full accepted window.
- Enter the range as written in your manifest.
- Test a specific version against it, or read the full accepted range.
- Check the 0.x behaviour if the dependency is below 1.0.0.
- Test prerelease matching explicitly if you consume them.
The semver specification defines the version format itself. Other developer tools are in the tools directory.
Frequently asked questions
What is the difference between caret and tilde?
Caret allows minor and patch updates; tilde allows only patch. ^1.2.3 accepts 1.9.0 while ~1.2.3 stops at 1.2.x.
Why does the caret behave differently below 1.0.0?
Because 0.x is explicitly unstable, so a minor bump may break things. ^0.5.2 therefore behaves like a tilde and allows only 0.5.x.
Are prereleases included in ranges?
Not by default. ^1.2.3 does not match 1.9.0-beta.1. Matching a prerelease requires a range that mentions one, which is deliberate.
Final thought
Check the 0.x case explicitly. Most semver surprises are a caret on a pre-1.0 dependency behaving exactly as specified and not at all as expected.