Back to blog
Sep 10, 2026
5 min read

What the version number is actually trying to tell you

You update a package and your app breaks, or you are too scared to update anything at all. The three numbers in 2.14.3 were telling you whether that update was safe, if you knew how to read them. Here is semantic versioning, the caret and tilde that trip everyone up, and why the whole promise rests on trust.

You run an update, and your app breaks. Or the opposite: you are scared to update anything, so your project quietly rots on old versions. Both come from the same gap, not knowing how to read a version number. That string like 2.14.3 is not decoration. Under a scheme called semantic versioning (SemVer for short), each number is a deliberate message from the author about whether it is safe for you to upgrade. Once you can read it, updating stops being a gamble.

Three numbers, three different promises

A semantic version is MAJOR.MINOR.PATCH, and each part promises something different about the change:

  • PATCH (2.14.2 to 2.14.3): a backward-compatible bug fix. Nothing new, nothing removed, just something broken now works. Safe.
  • MINOR (2.14.3 to 2.15.0): a backward-compatible new feature. There is more than before, but everything you already used still works. Safe.
  • MAJOR (2.15.0 to 3.0.0): a breaking change. Something you relied on changed or was removed, and your code might break. Stop and read the notes.

Think of it like editions of a book. A patch fixes a typo, so the new printing is the same book. A minor version adds a chapter, so your old page references still work, there is just more. A major version is a rewrite that renumbers every page, so your careful reference to “page 40” now points at the wrong thing. The number tells you which kind of change you are about to accept.

flowchart TD
  C[You made a change] --> Q{What kind?}
  Q -->|fixed a bug, nothing else| P[Bump PATCH: 2.14.3 to 2.14.4]
  Q -->|added a feature, nothing breaks| M[Bump MINOR: 2.14.3 to 2.15.0]
  Q -->|removed or changed something| J[Bump MAJOR: 2.14.3 to 3.0.0]

NOTE

Bumping one number resets the ones after it. 2.14.3 with a breaking change becomes 3.0.0, not 3.14.3. A new major wipes the minor and patch back to zero, because it is a fresh start.

The caret and the tilde decide what gets upgraded

Here is where beginners actually get bitten, in package.json. You rarely pin one exact version; you give a range, and two symbols do most of the work. They decide, every time you install, how new a version your project is allowed to jump to on its own.

You writeYou allowMeaning
^1.2.3 (caret)>=1.2.3 <2.0.0Patches and minors, never a new major. The default.
~1.2.3 (tilde)>=1.2.3 <1.3.0Patches only. More cautious.
1.2.3 (exact)only 1.2.3Nothing moves. Most locked.

The caret is the default for a reason: if everyone follows SemVer, then minor and patch updates are safe by promise, and the caret grabs those bug fixes and features while refusing to cross a major bump that could break you. The tilde is stricter, taking only patch fixes. Both exist so you get security and bug fixes automatically without silently swallowing a breaking change.

Below 1.0, all bets are off

One trap worth knowing. Any version starting with 0. (like 0.4.2) means initial development, not yet stable. In this range the author is explicitly reserving the right to break things in any release, so the normal promises do not hold. Tooling knows this: ^0.4.2 does not allow 0.5.0, because at 0.x a minor bump is treated as potentially breaking. When you depend on a 0.x package, expect surprises and pin more tightly.

There is also the pre-release tag, like 1.0.0-beta.2, for versions that are not ready for general use. These are not installed unless you ask for them by name, which keeps unstable code out of your project by default.

The promise only works if people keep it

Now the honest part, and the reason this connects to everything else on this blog. SemVer is a promise made by humans, and humans break it. Somebody ships a “patch” that quietly changes behavior, and your build that trusted the caret breaks anyway. The number is a strong signal, not a guarantee.

That is exactly why lockfiles exist. A file like package-lock.json records the exact version of every dependency that was actually installed, so the next person, and your server, get byte-for-byte the same versions you tested, not just “something in the range”. You commit it. The range says what you are willing to accept; the lockfile pins what you actually shipped.

IMPORTANT

Commit your lockfile, and treat a dependency update like any other change: let it happen on a branch, run your tests, and read the changelog on a major bump. Automated tools can open these updates for you, which ties straight into the automation that keeps an app alive. Semantic versioning is what makes trusting thousands of other people’s code possible at all; the lockfile is what makes it safe when someone breaks the promise.

The tools you actually reach for

JobPopular toolsNote
The spec itselfsemver.orgFree; the one-page rulebook worth reading once
Reproducible installspackage-lock.json, pnpm-lock.yaml, yarn.lockCommit it; it pins exact versions
Automating version bumpssemantic-release, ChangesetsFree/OSS; sets the version from your commit messages
Automating the updatesRenovate, DependabotFree; opens the PR, you review and merge

Read the number before you upgrade

The lesson is that a version number is a message, and most developers never learn to read it. Once you do, 2.14.3 to 2.14.4 is a shrug, 2.14.3 to 2.15.0 is a safe “nice, new stuff”, and 2.14.3 to 3.0.0 is a “clear an afternoon and read the migration guide”. You stop fearing updates and you stop being reckless with them, because the number told you which one this is. And you keep a lockfile, because the one time the promise is broken should not be the time it reaches production.

The number is a message. Learn to read it before you install.

Sources

Read next