When I first heard about Semantic Versioning, or SemVer, I thought it was one of those ideas that’s so obviously right that we were all going to benefit from someone having just codified it and written it down.
In a nutshell, it’s the idea that your software should be versioned with a three-part string in the format MAJOR.MINOR.PATCH, each being an integer — for example, version 1.13.3. If you fix a bug you increment the patchlevel for the resulting new release (yielding 1.13.4 in this case). If you add a backwards-compatible feature you increment the minor level and reset the patchlevel to zero (yielding 1.14.0). And if you have to make a backwards-incompatible change, you increment the major version and reset everything else (yielding 2.0.0).
This is good because it means downstream authors whose software consumes yours can safely upgrade when the patchlevel increments; and also when the minor version increments, though they might want to read the release notes to see if any of the new stuff is useful. But they know not to blindly upgrade when the major version increments, because the new version will not be backwards-compatible. Upgrading to a new major version of a dependency is significant work.
Here’s the problem: people interpret SemVer as permission to make breaking changes.
Back when software engineers used to have a bit of respect for actual engineering, a major release was, well, a major event. One that would be avoided whenever possible, because one of the very first tenets of software engineering is that you keep your interfaces backwards compatible. It’s just basic decency.
Back in The Old Days, Unix libraries (libc included) didn’t even have version numbers. Because they never made backwards-incompatible changes. So software written in 1990 would Just Work in 2000 — as it should, in any sane universe.
Now? New major version of some significant dependency arrive virtually every week. A lot of my work is the JavaScript/Node ecosystem. Node itself is on major version 21. Babel is on major version 7. React is on major version 18 and even something as trivial as react-router is on its sixth major version. Just think about that! A piece of software so trivial that its only job is to say “when serving the page at route /foo/bar/baz, use the React component <BazHandler>” has thrown its arms up in the air and completely changed its API five times since v1.0.0 came out it 2015. That’s five major changes in eight years.
And this disease is catching. A program that I am slightly involved in maintaining as one of the tangential parts of my day job was first released as v2.0.0 in 2017 and is now on v27.0.0. That’s 25 breaking changes in six years. The maintainance burden is intolerable.
Listen up, kids. Cool APIs don’t change. [See also].
To say it another way: If you need to make a breaking change to your API, it means you screwed up. Don’t screw up.
And don’t think just marking your breaking change with a new major version number makes it OK. It does not. All it means is that people are unlikely to accidentally upgrade to your incompatible version. It still leaves them with the burden of reading all your release notes, figuring out what changes are relevant to their code, thinking through what changes that requires them to make to their own work, making those changes and verifying that nothing broke.
Notes
- Yes, I know that React stayed in the v0.x.y space all the way up to v0.14.8, then leapt straight to v15.0.0. That makes no difference: it’s still 17 breaking changes.
- Yes, no doubt react-router does have all kinds of fancy extra facilities that I don’t need. That’s no excuse for five breaking changes to the trivially simple core. When grown-ups add new features, they add them: they don’t change existing ones.
- This is by no means limited to the world of JavaScript/Node: the modules I mentioned that’s on major version 27 is written in Java, using the Maven dependency system.
This entry was posted in Bad habits, Programming. Bookmark the permalink.

