Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Fixing a Wrong latest Dist-Tag

When latest points at the wrong version, every new npm install your-lib gets that version: a release candidate, a canary build, or an old maintenance release that moved latest backwards. Because dist-tags are pointers rather than published versions, the fix is quick — move the tag — but the damage spreads fast, and some of it lands in lockfiles that will not heal on their own. This guide shows how to confirm the problem, repair the tag safely, deal with the consequences for users who installed during the window, and stop it from happening again.

Exact symptoms

Users report unexpected versions after a fresh install:

$ npm install your-lib
added 1 package in 2s
$ npm ls your-lib
app@1.0.0 /app
└── your-lib@4.0.0-rc.2
# package.json written by that install
"your-lib": "^4.0.0-rc.2"

Or, after a maintenance release, users on the current major suddenly get an older one:

npm view your-lib dist-tags
# { latest: '3.8.4', next: '4.1.0-rc.0' }
npm view your-lib versions --json | tail -3
# [ '3.8.4', '4.0.0', '4.0.1' ]   <- 4.0.1 exists but latest points at 3.8.4

Root cause analysis

npm publish without --tag sets latest to the version being published, whatever that version is. Three workflows produce a wrong latest this way. The channel model is covered in Release Channels and Dist-Tags.

How latest ends up on the wrong version A prerelease published without a tag, a canary published by a PR workflow, or a backport release from a maintenance branch each moves latest implicitly. npm publish without --tag latest moves to this version RC becomes latest 4.0.0-rc.2 for everyone prerelease Experimental build 0.0.0-canary.<sha> canary latest goes backwards 3.8.4 after 4.0.1 backport
Every path is the same mistake — a publish that did not specify its channel.
  1. A prerelease without a tag. Older npm versions publish 4.0.0-rc.2 as latest if --tag is missing; recent npm 11 releases refuse, but CI images with older npm still exist.
  2. A canary or snapshot workflow that forgot --tag canary.
  3. A backport release from a maintenance branch — 3.8.4 published after 4.0.1 — without an LTS tag. npm does not stop latest moving to a lower version.

Why npm lets this happen

It is reasonable to ask why the registry allows latest to move to a prerelease or an older version at all. The answer is that both are occasionally legitimate: some projects deliberately keep latest on a prerelease line during long beta periods, and rolling back to an older version is a valid response to a broken release. npm therefore treats the tag as the publisher's decision. Recent npm clients add a guard for the most common accident — refusing to publish a prerelease without an explicit tag — but they cannot know whether a lower version on latest is intentional. That makes the release workflow, not the registry, the right place for policy: encode which branches and versions may use which tags, and fail the job when a publish would break the rule.

Fixing the tag

Move latest to the correct version, and put the misplaced version where it belongs:

# Confirm current state
npm view your-lib dist-tags
npm view your-lib versions --json

# Point latest back at the correct stable version
npm dist-tag add your-lib@3.9.1 latest

# Re-home the misplaced version
npm dist-tag add your-lib@4.0.0-rc.2 next      # prerelease case
npm dist-tag add your-lib@3.8.4 v3-lts         # backport case

# Verify
npm view your-lib dist-tags

Changing a dist-tag is a write operation: it requires publish rights and, for packages that require 2FA, an OTP or the trusted CI identity — see Fixing npm EOTP One-Time Password Errors. Do not unpublish the misplaced version: it is a legitimate version, and users may already depend on it.

Recovering from a wrong latest tag Move latest to the correct version, re-home the misplaced version under its channel tag, deprecate if needed, fix the workflow, and communicate. npm dist-tag add @correct latest new installs fixed immediately caches may lag a few minutes re-home misplaced version next / canary / v3-lts deprecate if unsuitable message tells users what to install fix the workflow tag derived from version + branch announce issue / release notes
The tag fix takes seconds; follow it with a deprecation notice and a workflow fix so users and future releases are covered.

Cleaning up after the window

Moving the tag fixes new installs. It does not fix installs that happened while the tag was wrong:

  • Projects that ran npm install your-lib during the window recorded the misplaced version in package.json (as ^4.0.0-rc.2) and in their lockfile. A caret range on a prerelease keeps accepting later prereleases and the final 4.0.0, so those users will not return to 3.x on their own.
  • Projects with existing ranges such as ^3.8.0 were not affected by a misplaced prerelease, because ranges never match prereleases unless they name one; their lockfiles regenerate to the highest stable 3.x as before. In the backport case, a latest that moved backwards affects only bare installs by name; ranges still resolve to the highest matching version.

To reach affected users, deprecate the misplaced version with a clear message if it should not be used:

npm deprecate your-lib@4.0.0-rc.2 "Published to latest by mistake. Install your-lib@3.9.1, or your-lib@next to test 4.0."

npm prints the message on every install of that version, which is the only channel that reaches users who never read your issue tracker. Deprecation is covered in Deprecating npm Package Versions.

Who is affected by a wrong latest tag Compares bare installs, caret ranges on the stable line, prerelease ranges written during the window, and existing lockfiles. affected? fix npm i your-lib during window yes, wrong version saved deprecation message + reinstall existing ^3.x range no (prereleases excluded) none ^4.0.0-rc.2 saved during window stays on 4.x prereleases change range or pin existing lockfiles no none
Bare installs during the window are the main casualties; existing ranges and lockfiles are mostly safe.

Rolling back a bad stable release

The same tag operation is useful on purpose. If a stable release turns out to be broken — a regression that affects many users — moving latest back to the previous version stops fresh installs from picking it up while you prepare a fix:

npm dist-tag add your-lib@3.9.0 latest
npm deprecate your-lib@3.9.1 "Regression in date parsing; use 3.9.0 or wait for 3.9.2"

Understand the limits before relying on it. Moving latest changes only bare installs; every consumer with a range like ^3.9.0 still resolves to 3.9.1, because ranges follow versions, not tags. The deprecation message is what reaches them. The real fix is a new patch release — 3.9.2 — which moves latest forward again and satisfies every range. Treat the rollback as a stopgap for the hours between discovering the problem and shipping the fix, and never try to "fix" a bad version by unpublishing it, which breaks anyone whose lockfile already references it.

Auditing tags across many packages

Organisations with dozens of packages benefit from a scheduled check that compares each package's tags with expectations: latest should be the highest non-prerelease version on the current major, prerelease tags should point at prerelease versions, and LTS tags should point at their major's highest version. A short script can do this:

for pkg in @acme/core @acme/react @acme/cli; do
  latest=$(npm view "$pkg" dist-tags.latest)
  highest=$(npm view "$pkg" versions --json | node -e '
    const cmp = (a, b) => { const x = a.split(".").map(Number), y = b.split(".").map(Number);
      for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]; return 0; };
    const v = JSON.parse(require("fs").readFileSync(0, "utf8")).filter(x => !x.includes("-")).sort(cmp);
    console.log(v[v.length - 1]);')
  [ "$latest" = "$highest" ] || echo "$pkg: latest=$latest but highest stable=$highest"
done

The versions list from the registry is in publish order, not version order, so the script sorts it numerically before picking the highest — otherwise a backport published last would look like the newest release. The check would have flagged both the release-candidate and the backport examples above within a day, even if nobody had reported them.

Worked example: a Friday release candidate

A maintainer publishes 4.0.0-rc.2 from a CI image with npm 9, and the workflow has no --tag. Within an hour, issue reports arrive from users whose new projects fail on removed APIs. The maintainer runs npm dist-tag add your-lib@3.9.1 latest and npm dist-tag add your-lib@4.0.0-rc.2 next, deprecates 4.0.0-rc.2 with a message pointing to 3.9.1, and pins a comment on the issue. On Monday, the workflow is changed to compute the tag from the version and to fail when a prerelease would use latest, and the CI image is updated to npm 11, which refuses untagged prerelease publishes as a second line of defence.

Prevention and CI/CD guardrails

  • Compute the tag from the version and branch in CI; never rely on the default.
  • Fail the job if a prerelease or non-main branch would publish to latest.
  • Use npm 11 or later in release jobs, which rejects untagged prerelease publishes.
  • Print npm dist-tag ls after every publish so a wrong tag is visible in the release log.

Frequently Asked Questions

Does moving latest affect existing users' lockfiles? No. Lockfiles pin exact versions. Only new resolutions — fresh installs, updates, lockfile regeneration — look at tags or ranges.

Can I move latest to an older version deliberately? Yes. That is a legitimate way to roll back a bad stable release while a fix is prepared. Remember that ranges still resolve to the highest matching version, so also deprecate the bad version.

How long until the fix reaches everyone? The registry updates immediately; client and proxy caches may serve the old tag for a short time. Users behind corporate proxies may need their proxy's metadata cache refreshed.

Can other maintainers see who moved a tag? The registry does not show a public history of tag changes. Keep tag moves inside CI, where logs record who triggered them and why, and mention manual moves in the release notes or the related issue.

Should the LTS line ever own latest? No. latest should always point at the newest stable release of the current major. Maintenance lines get their own tags so their releases never move latest backwards.

Related

Release Channels and Dist-Tags