yarn upgrade updates packages to the newest version that still satisfies the range already written in package.json. It does not change that range. So if your package.json says ^4.18.0 and version 5 is out, yarn upgrade will take you to 4.18.9 and leave you wondering why nothing happened.
That is the single most common confusion with Yarn’s upgrade commands, and it is not a bug — it is the range doing its job. Understanding which command modifies package.json and which only modifies the lockfile turns dependency updates from guesswork into something you can reason about, and it is the difference between a routine upgrade and an accidental major version bump.
Table of contents
- The commands, and what each one touches
- The interactive upgrade, which is the one to use
- What the lockfile is for
- Resolutions, for when a transitive dependency is the problem
- Auditing and the supply chain
- A short reference
- How this fits the rest of the stack
- FAQ
The commands, and what each one touches
# newest version matching the existing range; lockfile only
yarn upgrade
yarn upgrade lodash
# newest version regardless of range; rewrites package.json
yarn upgrade --latest
yarn upgrade lodash --latest
# a specific version or dist-tag
yarn upgrade express@4.18.2
yarn upgrade express@next
# scoped to a group
yarn upgrade --scope @myorg
The distinction that matters: plain yarn upgrade only changes yarn.lock. yarn upgrade --latest also rewrites the version range in package.json. One is a within-the-rules refresh; the other is a decision to move to a new major.
So the answer to why did yarn upgrade not update my package.json is that it was never going to. That is --latest’s job.
Modern Yarn (version 2 and later, the Berry line) renamed things. yarn up replaces yarn upgrade and behaves slightly differently — it updates package.json by default, which is the opposite of the classic behaviour and a genuine trap when moving between versions:
# modern Yarn
yarn up lodash
yarn up 'lodash@^4'
yarn up -i lodash # interactive picker
yarn upgrade-interactive
Check which you are running with yarn --version before trusting any advice, including this. A 1.x version is classic Yarn; 3.x or 4.x is Berry, and the command names and defaults differ.
The interactive upgrade, which is the one to use
# classic Yarn
yarn upgrade-interactive
yarn upgrade-interactive --latest
# modern Yarn
yarn upgrade-interactive
This is the command worth building a habit around. It presents every outdated dependency grouped by how far behind it is, marks which updates cross a major version boundary, and lets you select individual packages with the arrow keys.
The value is the grouping. Patch and minor updates are usually safe to take in bulk; major updates need reading release notes first. Doing them in separate commits means that when something breaks, the commit that broke it is obvious.
A workflow that holds up well:
- Run
yarn upgrade-interactive, take all the patch updates, run the tests, commit. - Run it again, take the minor updates, run the tests, commit.
- Handle each major update individually — read the changelog, upgrade one package, fix what breaks, commit.
Three commits instead of one, and a bisectable history when something surfaces two weeks later.
What the lockfile is for
yarn.lock records the exact resolved version of every package, including transitive dependencies you never named. It is what makes an install reproducible.
- Commit it. Always, including in libraries. The argument that libraries should not commit lockfiles is about what consumers resolve, not about your own CI — and your CI benefits from determinism.
yarn install --frozen-lockfile(classic) oryarn install --immutable(modern) fails if the lockfile would need changing. This is what CI should run. It catches the case where someone editedpackage.jsonwithout updating the lock.- Do not edit it by hand. Resolve conflicts by taking either side and re-running install, which regenerates it correctly.
- Read the diff on upgrades. A one-package upgrade producing four hundred changed lines means it pulled in a lot more than you thought, and that is worth knowing before merging.
That last habit catches things nothing else will. A minor version bump of one dependency that rewrites half the lockfile has changed your transitive tree substantially, and a compromised or abandoned package can arrive that way without ever appearing in package.json.
Resolutions, for when a transitive dependency is the problem
Sometimes the package you need to update is not one you depend on directly. A security advisory names a library that is three levels down, and the package that depends on it has not released a fix.
{
"resolutions": {
"minimist": "^1.2.8",
"some-package/**/lodash": "^4.17.21"
}
}
resolutions forces a specific version everywhere in the tree, overriding what intermediate packages asked for. It is the escape hatch for exactly this situation, and npm has an equivalent in overrides.
It is also a lie you are telling your dependency tree, so use it deliberately. If the intermediate package genuinely required the old version’s behaviour, forcing a new one produces a subtle runtime break rather than an install error. Test after applying one, and add a comment or an issue so it gets removed when upstream catches up.
Check what actually got installed afterwards with yarn why <package>, which prints the dependency chain that pulled it in and the version resolved.
Auditing and the supply chain
yarn audit
yarn audit --level high
yarn npm audit # modern Yarn
yarn outdated
yarn why suspicious-package
Run yarn audit in CI and fail on high severity. It is free and it catches the class of problem that otherwise gets discovered from the outside.
One habit worth adopting given how npm-ecosystem attacks have gone recently: do not upgrade to a package version published in the last day or two unless you need to. Malicious releases are typically caught within hours, so a short delay between publication and adoption removes most of the exposure at essentially no cost. Some tooling offers this as a cooldown setting.
Also treat lifecycle scripts as the risk they are. postinstall scripts run arbitrary code on your machine and in your CI. Modern Yarn is stricter about this than classic Yarn; if your tooling supports disabling scripts by default and allowing them per package, it is worth the small setup cost.
A short reference
- Refresh within existing ranges —
yarn upgrade(classic). Lockfile only. - Move to newest, rewriting package.json —
yarn upgrade --latest(classic),yarn up(modern). - Choose interactively —
yarn upgrade-interactive, the best default for routine maintenance. - Pin an exact version —
yarn upgrade pkg@1.2.3. - Force a transitive version —
resolutionsin package.json. - Reproducible install in CI —
--frozen-lockfileor--immutable. - Find out why a package is present —
yarn why pkg. - Check for vulnerabilities —
yarn audit --level high.
And keep dependency updates on a schedule rather than doing them when something forces you to. A fortnightly interactive pass with patches taken in bulk is far less work than a six-month gap where every package needs a major version jump at once.
How this fits the rest of the stack
Everything above only produces a predictable result if the build that runs yarn install uses the committed lockfile and a pinned Node version — otherwise the reproducibility the lockfile promises stops at your laptop. A service that builds from its repository enforces exactly that, keeps the install log attached to the deploy it produced, and leaves the previous deploy available when an upgrade turns out to be the cause. Node services on RunxBuild covers the runtime and build configuration, and builds on RunxBuild covers what happens on each push. When you are sizing an application and its database, the RunxBuild hosting calculator prices the service, database, storage, and bandwidth separately.
Useful related references:
- Should package-lock.json Be Committed?
- Update Node Version Without Breaking Your Deploy
- Ubuntu Update: apt update and apt upgrade Do Two Different Jobs
- Services on RunxBuild
FAQ
Why does yarn upgrade not update package.json?
Because plain yarn upgrade only moves you to the newest version that satisfies the range already in package.json, and updates the lockfile. To change the range itself and move across a major version, use yarn upgrade --latest in classic Yarn or yarn up in modern Yarn.
What is the difference between yarn upgrade and yarn up?
yarn upgrade is classic Yarn 1 and only changes the lockfile by default. yarn up is modern Yarn (2 and later) and updates package.json by default — the opposite behaviour. Check yarn --version before following any advice, since the defaults differ.
How do I update a single package with Yarn?
yarn upgrade package-name for the newest version within the existing range, or yarn upgrade package-name --latest to move beyond it. To pin an exact version, use yarn upgrade package-name@1.2.3.
How do I force a version of a transitive dependency?
Add a resolutions entry to package.json, which forces that version everywhere in the tree regardless of what intermediate packages requested. npm’s equivalent is overrides. Test afterwards — you are overriding a constraint another package declared, which can break it subtly.
Should I commit yarn.lock?
Yes, including for libraries. It records the exact resolved version of every package including transitive ones, which is what makes installs reproducible. In CI, run yarn install --frozen-lockfile or --immutable so a mismatch fails the build rather than silently resolving something new.