A security scanner flags minimatch and asks for 11.0.1. You run npm update and nothing changes. The reason is one character in someone else's package.json: eslint 10.14.0 depends on "minimatch": "^10.2.6", and a caret range never crosses a major version. This guide explains what ^ and ~ actually allow, how prereleases and lockfiles change the picture, and what you can do when the fix you need is out of range.
To test any range as you read, open the Semver Range Checker. It shows what a range desugars to, marks each version you paste as in or out with the reason, and shows which one npm install would pick.
SemVer in one minute
A version is MAJOR.MINOR.PATCH. The Semantic Versioning 2.0.0 spec says to bump MAJOR for incompatible API changes, MINOR for backward-compatible features and PATCH for backward-compatible bug fixes. Ranges in package.json rely on publishers following that promise.
Two optional suffixes follow the patch number:
- Prerelease, after a hyphen:
2.0.0-beta.2. A prerelease ranks below its release. Identifiers made of digits compare as numbers, others in ASCII order, and numeric ones rank below alphanumeric ones, so1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. - Build metadata, after a plus:
1.2.3+build.5. The spec says it must be ignored when working out precedence, and npm strips it from ranges.
The spec also says major version zero is for initial development: anything may change at any time. That rule explains the 0.x behaviour below.
What ^ and ~ mean
npm turns every range into plain comparators before it compares anything. The caret keeps the left-most non-zero number fixed. The tilde keeps the minor number fixed when you give one. The -0 on each upper bound is the lowest possible prerelease, so <2.0.0-0 also shuts out 2.0.0-beta.1.
| Range | Desugars to | Allows |
|---|---|---|
^1.2.3 | >=1.2.3 <2.0.0-0 | Minor and patch updates |
~1.2.3 | >=1.2.3 <1.3.0-0 | Patch updates |
^1.2 | >=1.2.0 <2.0.0-0 | Minor and patch updates |
~1.2 | >=1.2.0 <1.3.0-0 | Patch updates |
^1, ~1 | >=1.0.0 <2.0.0-0 | Anything in 1.x |
^0.2.3 | >=0.2.3 <0.3.0-0 | Patch updates only |
~0.2.3 | >=0.2.3 <0.3.0-0 | Patch updates only |
^0.0.3 | >=0.0.3 <0.0.4-0 | Only 0.0.3 |
^0.x | <1.0.0-0 | Anything in 0.x |
For a full version at 1.0.0 or above, the difference is simple: ^ takes new minor versions and ~ doesn't. On 0.x they agree, because a 0.x minor release counts as breaking: ^0.2.3 and ~0.2.3 both stop before 0.3.0. On 0.0.x the caret is stricter still. ^0.0.3 allows only 0.0.3, while ~0.0.3 allows any 0.0.x from 0.0.3 up. npm install <pkg> saves a caret range by default, because the save-prefix setting defaults to ^. Set save-exact if you want exact versions instead.
x-ranges, hyphen ranges and combining them
An x, X or * stands for any number in that place, and a missing part counts as an x. 1.x, 1.* and 1 all mean >=1.0.0 <2.0.0-0. A bare * or an empty string means any version.
A hyphen range includes both ends. A partial upper bound takes in everything it names, and a partial lower bound is filled with zeros:
1.2.3 - 2.3.4 → >=1.2.3 <=2.3.4
1.2.3 - 2.3 → >=1.2.3 <2.4.0-0
1.2 - 2.3.4 → >=1.2.0 <=2.3.4
Comparators separated by a space must all match. || separates alternatives, and a version matches if it satisfies any one of them:
>=1.2.7 <1.3.0 || ^2.0.0
The dash needs a space on each side. Without the spaces, 1.2.3-2.0.0 is a single prerelease version, not a range.
Prereleases only match when you ask for them
A prerelease is inside >=1.0.0 by precedence, yet npm won't install 2.0.0-beta.1 for that range. The rule in node-semver, the library npm uses: a prerelease matches only if some comparator in the same alternative names a prerelease of the same major.minor.patch.
^1.2.3-beta.2accepts1.2.3-beta.3,1.2.3and1.3.0.- It rejects
1.3.0-beta.1: no comparator names a 1.3.0 prerelease. >=1.0.0rejects every prerelease.
So opting in to one beta doesn't opt you in to the betas of every later version. node-semver has an includePrerelease option that drops the rule, but npm install doesn't use it. With that option on, the -0 bounds are what keep a beta of the next major out of a caret range.
Lockfiles decide what actually gets installed
The range in package.json is only half the story. package-lock.json records the exact tree npm installed, so later installs produce the same tree. Commit it.
| Command | What it does with ranges |
|---|---|
npm install | Uses the locked versions while they still satisfy package.json. If a range no longer accepts the locked version, package.json wins and the lockfile is updated. |
npm ci | Installs exactly what the lockfile says, after deleting node_modules. If package.json and the lockfile disagree, it stops with an error. It never writes either file. |
npm update | Moves each package to the newest version its range allows, respecting the ranges of packages that depend on it. It leaves package.json alone unless you add --save. |
Without a lockfile, npm first tries the version tagged latest if the range accepts it, then the highest version that matches, passing over deprecated ones. If nothing matches, it stops with notarget No matching version found. See npm install, npm ci and npm update for the details.
When the fix is in a new major
Back to minimatch. eslint asks for ^10.2.6, which means >=10.2.6 <11.0.0-0. 11.0.1 is above that bound, so neither npm update nor npm audit fix will install it under eslint, however many times you run them. Paste ^10.2.6 and the output of npm view minimatch versions --json into the checker to see the whole picture at once. You have three ways out.
Upgrade or wait for the package that declares the range
The clean fix is a release of eslint that depends on minimatch 11. Check its changelog, upgrade when one exists, or open an issue asking for it. Its maintainers can test the new major. You can't, at least not fully.
Force the version with overrides
The overrides field in your root package.json replaces a version anywhere in the tree:
{
"overrides": {
"minimatch": "11.0.1"
}
}
To limit it to one path, nest it: { "overrides": { "eslint": { "minimatch": "11.0.1" } } }. npm reads overrides only from the root package.json, and you can't override a package you depend on directly unless the specs match. Use "$minimatch" to refer to your own range. An override puts a major version under code that was never tested with it, so run your linting and tests before you merge.
Remove the dependency
If nothing uses it, npm uninstall it. A dependency you don't have can't be vulnerable. For a dev tool that nobody runs any more, this is often the quickest fix.
Other ecosystems read ^ and ~ differently
The symbols look universal, but the meanings differ:
- Cargo treats a bare
"1.2.3"in Cargo.toml as^1.2.3. Its caret and tilde match npm's, including the 0.x rules, and it separates comparators with commas:">= 1.2, < 1.5". See the Cargo reference. - Composer reads
^like npm. Its~1.2means>=1.2 <2.0.0, while npm's~1.2stops before 1.3.0. Composer's~1.2.3stops before 1.3.0, like npm's. See Composer's versions article.
So a tilde range copied from composer.json into package.json can mean something narrower. npm itself accepts ~>1.2 as another spelling of ~1.2.
Next steps
Check the ranges that matter to you in the Semver Range Checker, using its Compare and Bump modes for release planning. To see which ranges changed between two versions of package.json, paste both into JSON Diff. To read the full output of npm view <pkg> --json, use the JSON Formatter.