Managing Prerelease and dist-tags on npm
Publish beta, next, and canary builds safely with npm dist-tags and semver prerelease identifiers without disrupting the default latest tag.
Publishing a beta build the same way you publish a stable release has one dangerous default: npm publish applies the latest dist-tag unless told otherwise, meaning npm install my-lib — with no version specified — would install your beta to every new user. This is independent of the semver string itself; a version like 2.1.0-beta.0 is not automatically excluded from latest just because it contains a prerelease identifier. Getting this right requires understanding two separate but related mechanisms: semver prerelease identifiers in the version string, and npm’s dist-tag system that decides which version npm install resolves to by default.
Root Cause
Semver prerelease identifiers (the -beta.0, -rc.1, -alpha.3 suffix) exist purely as a version-ordering convention — they tell semver-aware tooling that 2.1.0-beta.0 sorts before 2.1.0, so dependency ranges like ^2.0.0 will not match a prerelease unless a consumer opts in explicitly. dist-tags are a completely separate system: they’re mutable named pointers npm stores per-package, mapping a tag name (latest, next, beta) to one specific version. npm install my-lib with no version specifier resolves through the latest tag, not through “the highest semver version” — so a prerelease version published without --tag still becomes the default install the moment it’s the most recently published version under latest.
Minimal Reproduction
Publishing a beta without specifying a tag:
npm version 2.1.0-beta.0 --no-git-tag-version
npm publish
$ npm dist-tag ls my-lib
latest: 2.1.0-beta.0
Every consumer running npm install my-lib from this point forward receives the beta — there was no prerelease-aware guard rail. The fix requires an explicit tag at publish time.
dist-tags vs. Semver Prerelease
Step-by-Step Fix
Step 1 — Bump with an explicit prerelease identifier
npm version prerelease --preid=beta
# 2.0.0 -> 2.0.1-beta.0
Running npm version prerelease --preid=beta again increments the prerelease number rather than the patch version:
npm version prerelease --preid=beta
# 2.0.1-beta.0 -> 2.0.1-beta.1
Step 2 — Publish with an explicit --tag, never the default
npm publish --tag beta
$ npm dist-tag ls my-lib
latest: 2.0.0
beta: 2.0.1-beta.1
Consumers who want the preview install it explicitly:
npm install my-lib@beta
HAZARD PREVENTION
Symptom:
npm install my-libstarts resolving a prerelease version for every new user, with no one having asked for it.Root cause:
npm publishwas run without--tag, so npm applied its default tag —latest— to a version string that happened to include a prerelease identifier. The identifier alone does not opt a version out oflatest.Fix: Always pass
--tag <name>explicitly whenever publishing a version with a prerelease identifier. Treat a barenpm publishas reserved for stable releases only, and enforce this in CI rather than relying on a human to remember the flag.
Step 3 — Automate this with Changesets’ pre mode
Manually tracking prerelease numbers and tags across a team is error-prone; Changesets has a dedicated mode for it:
npx changeset pre enter beta
npx changeset version
npx changeset publish
While in pre mode, every changeset version run appends the next prerelease number automatically, and changeset publish applies the beta dist-tag without any manual --tag flag. Exit pre mode to resume normal stable releases:
npx changeset pre exit
npx changeset version
npx changeset publish
Step 4 — Reassign a tag without republishing, if needed
If a version was already published under the wrong tag, move the pointer directly — this does not republish or change any file content:
npm dist-tag add [email protected] next
npm dist-tag rm my-lib beta
Verification
npm dist-tag ls my-lib
Expected output showing latest untouched by prerelease activity:
latest: 2.0.0
beta: 2.0.1-beta.1
next: 2.1.0-rc.0
Confirm a plain install still resolves the stable version:
npm view my-lib version
# 2.0.0
Edge Cases / Gotchas
- CI publishing on every merge to a preview branch. If CI auto-publishes on every push to a
nextbranch, hard-code--tag nextin the workflow step rather than relying on contributors to remember it locally. - pnpm and Yarn respect npm’s dist-tags identically.
pnpm add my-lib@betaandyarn add my-lib@betaboth resolve through the same tag system — there is no package-manager-specific override for this behavior. - Deleting the last version under a tag. Removing every version tagged
betawithnpm unpublishleaves thebetatag dangling; runnpm dist-tag rm my-lib betaafterward to clean it up, since npm does not do this automatically. - Scoped package defaults. New scoped packages (
@acme/sdk) default to private/restricted access —npm publish --tag beta --access publicis required the first time, or the publish fails outright regardless of the tag.
Frequently Asked Questions
What happens if I run npm publish without a --tag flag on a prerelease version?
npm still applies the latest tag by default regardless of the version string containing a prerelease identifier like -beta.0. The version number and the dist-tag are entirely independent; you must pass --tag explicitly to avoid a preview build becoming the default install.
How do I promote a beta release to latest once it’s stable?
Publish a new version without a prerelease identifier (for example bumping 2.1.0-beta.3 to 2.1.0) and either let the default publish apply latest, or explicitly move the tag with npm dist-tag add [email protected] latest if the version was already published under another tag.
Can two dist-tags point at the same version?
Yes. npm dist-tag add [email protected] next can point next at the same version already tagged latest. Tags are just named pointers to a version string; nothing prevents multiple tags from resolving to the same release.
Does Changesets support publishing prerelease versions?
Yes, through its dedicated pre mode. Run npx changeset pre enter beta before versioning, then npx changeset version and npx changeset publish as usual — Changesets appends the prerelease suffix and applies the matching dist-tag automatically, and npx changeset pre exit returns to normal releases.
Related
- Automating Changelogs with Changesets — the pre-release mode referenced above, and how it fits into the broader version-and-publish workflow.
- Versioning & Changelog Automation — the full section this guide belongs to, covering standard (non-prerelease) version bumps.
- Automating npm Releases with GitHub Actions — the CI pipeline where
--tagflags and pre-mode commands are usually invoked automatically rather than by hand.