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.2to2.14.3): a backward-compatible bug fix. Nothing new, nothing removed, just something broken now works. Safe. - MINOR (
2.14.3to2.15.0): a backward-compatible new feature. There is more than before, but everything you already used still works. Safe. - MAJOR (
2.15.0to3.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 write | You allow | Meaning |
|---|---|---|
^1.2.3 (caret) | >=1.2.3 <2.0.0 | Patches and minors, never a new major. The default. |
~1.2.3 (tilde) | >=1.2.3 <1.3.0 | Patches only. More cautious. |
1.2.3 (exact) | only 1.2.3 | Nothing 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
| Job | Popular tools | Note |
|---|---|---|
| The spec itself | semver.org | Free; the one-page rulebook worth reading once |
| Reproducible installs | package-lock.json, pnpm-lock.yaml, yarn.lock | Commit it; it pins exact versions |
| Automating version bumps | semantic-release, Changesets | Free/OSS; sets the version from your commit messages |
| Automating the updates | Renovate, Dependabot | Free; 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
- Semantic Versioning 2.0.0 (semver.org) for the MAJOR.MINOR.PATCH rules, resetting lower numbers, 0.x, and pre-releases
- Baeldung: a guide to semantic versioning for what each number promises and backward compatibility
- NodeSource: semver tilde and caret for how
^and~expand into version ranges - OneUptime: how to implement semantic versioning automation for lockfiles and automating version bumps from commit messages