Use caret for most direct npm dependencies, tilde when you only want patch updates, and an exact version for packages you cannot afford to surprise you. Whatever you write in package.json, the committed package-lock.json decides what actually installs, so treat the range as a policy and the lockfile as the record.
What each range allows
npm's own documentation defines the three families by the update type they accept. Patch ranges look like ~1.0.4, 1.0 or 1.0.x. Minor ranges look like ^1.0.4, 1 or 1.x. Major ranges, written * or x, accept everything.
| Written as | Accepts | Typical use |
|---|---|---|
| Exact, such as 1.0.4 | Only that version | Critical or fragile packages |
| Tilde, such as ~1.0.4 | Patch releases of 1.0 | Conservative, bug fixes only |
| Caret, such as ^1.0.4 | Minor and patch releases of 1 | Default for most packages |
| Star or x | Any version | Almost never appropriate |
These rules assume maintainers follow semantic versioning: the first number changes for breaking changes, the middle for compatible features, the last for compatible fixes. That is a convention, not a guarantee, and a range trusts every future release that fits it.
The range is a policy, the lockfile is the record
A range answers "which future versions am I willing to accept?" A lockfile answers "which versions did we actually build with?" The npm documentation says package-lock.json describes the exact tree that was generated so later installs produce identical trees regardless of intermediate dependency updates, and that it is meant to be committed.
In CI and deploys, npm ci installs from that lockfile and exits with an error if the lockfile and package.json disagree, instead of quietly updating it. So a caret range in package.json does not mean your build floats. It means the next deliberate npm install or npm update may move the lockfile, and that move shows up as a diff you can review.
When to choose each
Pick caret when the package is well maintained, has tests you trust and you want fixes without manual bumps. This is the default npm writes for a reason.
Pick tilde when you want bug fixes but treat minor releases as changes that need a human look, for example a build tool or a framework core.
Pick exact when a package touches secrets, authentication, installs scripts, or has a history of breaking releases. You can have npm write exact versions for you with the save-exact option:
npm install --save-exact some-package
Avoid star and x on anything you ship. They accept every major version, including ones with breaking changes you have never tested.
A range accepts what the registry serves
There is a security angle to ranges. A range will accept any new release that fits, including a release published by someone who has taken over a maintainer account. The lockfile blunts this for builds that use npm ci, because the new version only enters when someone updates the lockfile and a reviewer sees it. Reading the lockfile diff in pull requests is therefore part of the control, not an optional extra.
Exact versions in package.json do not pin transitive dependencies on their own. For those, the lockfile does the work, and npm's overrides field can force a specific version of a nested package when you need to.
Where Safeguard fits
Safeguard's SBOM and dependency scanning shows which versions actually resolved in your tree and which known vulnerabilities apply to them, and you can try a free scan at gold.safeguard.sh/scan. Ranges decide what you may get; scanning tells you what you did get.
Try it yourself
Open the inventory and look at how a direct dependency expands into the transitive packages that a range quietly accepts.
Click through it at your own pace, or open it full screen.
Quick check
Does a caret range mean my CI build changes every day?
No, not if you install with npm ci. It uses the lockfile and fails if the lockfile and package.json disagree. The tree only changes when someone updates the lockfile.
Should I use exact versions everywhere?
Usually not. Exact versions slow down fixes and still leave transitive dependencies to the lockfile. Use them for the few packages where surprise is expensive.
Sources
- npm Docs: About semantic versioning
- npm Docs: npm-ci
- npm Docs: package-lock.json
- npm Docs: package.json (overrides)
Last reviewed: 19 October 2026.