Skip to content

Instantly share code, notes, and snippets.

@jashkenas
Last active June 19, 2025 18:41
Show Gist options
  • Save jashkenas/cbd2b088e20279ae2c8e to your computer and use it in GitHub Desktop.
Save jashkenas/cbd2b088e20279ae2c8e to your computer and use it in GitHub Desktop.
Why Semantic Versioning Isn't

Spurred by recent events (https://news.ycombinator.com/item?id=8244700), this is a quick set of jotted-down thoughts about the state of "Semantic" Versioning, and why we should be fighting the good fight against it.

For a long time in the history of software, version numbers indicated the relative progress and change in a given piece of software. A major release (1.x.x) was major, a minor release (x.1.x) was minor, and a patch release was just a small patch. You could evaluate a given piece of software by name + version, and get a feeling for how far away version 2.0.1 was from version 2.8.0.

But Semantic Versioning (henceforth, SemVer), as specified at http://semver.org/, changes this to prioritize a mechanistic understanding of a codebase over a human one. Any "breaking" change to the software must be accompanied with a new major version number. It's alright for robots, but bad for us.

SemVer tries to compress a huge amount of information — the nature of the change, the percentage of users that will be affected by the change, the severity of the change (Is it easy to fix my code? Or do I have to rewrite everything?) — into a single number. And unsurprisingly, it's impossible for that single number to contain enough meaningful information.

If your package has a minor change in behavior that will "break" for 1% of your users, is that a breaking change? Does that change if the number of affected users is 10%? or 20? How about if instead, it's only a small number of users that will have to change their code, but the change for them will be difficult? — a common event with deprecated unpopular features. Semantic versioning treats all of these scenarios in the same way, even though in a perfect world the consumers of your codebase should be reacting to them in quite different ways.

Breaking changes are no fun, and we should strive to avoid them when possible. To the extent that SemVer encourages us to avoid changing our public API, it's all for the better. But to the extent that SemVer encourages us to pretend like minor changes in behavior aren't happening all the time; and that it's safe to blindly update packages — it needs to be re-evaluated.

Some pieces of software are like icebergs: a small surface area that's visible, and a mountain of private code hidden beneath. For those types of packages, something like SemVer can be helpful. But much of the code on the web, and in repositories like npm, isn't code like that at all — there's a lot of surface area, and minor changes happen frequently.

Ultimately, SemVer is a false promise that appeals to many developers — the promise of pain-free, don't-have-to-think-about-it, updates to dependencies. But it simply isn't true. Node doesn't follow SemVer, Rails doesn't do it, Python doesn't do it, Ruby doesn't do it, jQuery doesn't (really) do it, even npm doesn't follow SemVer. There's a distinction that can be drawn here between large packages and tiny ones — but that only goes to show how inappropriate it is for a single number to "define" the compatibility of any large body of code. If you've ever had trouble reconciling your npm dependencies, then you know that it's a false promise. If you've ever depended on a package that attempted to do SemVer, you've missed out on getting updates that probably would have been lovely to get, because of a minor change in behavior that almost certainly wouldn't have affected you.

If at this point you're hopping on one foot and saying — wait a minute, Node is 0.x.x — SemVer allows pre-1.0 packages to change anything at any time! You're right! And you're also missing the forest for the trees! Keeping a system that's in heavy production use at pre-1.0 levels for many years is effectively the same thing as not using SemVer in the first place.

The responsible way to upgrade isn't to blindly pull in dependencies and assume that all is well just because a version number says so — the responsible way is to set aside five or ten minutes, every once in a while, to go through and update your dependencies, and make any minor changes that need to be made at that time. If an important security fix happens in a version that also contains a breaking change for your app — you still need to adjust your app to get the fix, right?

SemVer is woefully inadequate as a scheme that determines compatibility between two pieces of code — even a textual changelog is better. Perhaps a better automated compatibility scheme is possible. One based on matching type signatures against a public API, or comparing the runs of a project's public test suite — imagine a package manager that ran the test suite of the version you're currently using against the code of the version you'd like to upgrade to, and told you exactly what wasn't going to work. But SemVer isn't that. SemVer is pretty close to the most reductive compatibility check you would be able to dream up if you tried.

If you pretend like SemVer is going to save you from ever having to deal with a breaking change — you're going to be disappointed. It's better to keep version numbers that reflect the real state and progress of a project, use descriptive changelogs to mark and annotate changes in behavior as they occur, avoid creating breaking changes in the first place whenever possible, and responsibly update your dependencies instead of blindly doing so.

Basically, Romantic Versioning, not Semantic Versioning.

All that said, okay, okay, fine — Underscore 1.7.0 can be Underscore 2.0.0. Uncle.

(typed in haste, excuse any grammar-os, will correct later)

@christian-weiss
Copy link

@robnagler, i do not think "numpy" (from Python community) proves a point about "fast bumping major", as:
a) it does not use SemVer
b) collecting all breaking changes in one giant release opposed to release many smaller releases (each with breaking changes), is not related to a version string at all.

2. a. They hold back breaking changes, including super insignificant ones, and pile them up for an upcoming generation upgrade. This results in weird release schedules that are pulled out of thin air and only exist to worship the SemVer god but not bring convenience to developers or users. When a major release comes, developers have to face with dozens or hundreds of breaking changes at the same time, resulting in unbearable maintenance burden

Holding back changes can be done but nowerdays the industry (agile movement, extreme programming) tend to release often and in small chunks, as it helps to get faster feedback and to make it easyer to spot and fix issues faster.

That is not a version schema issue at all. You will experience pain with big releases independent from SemVer.

A SemVer version number is just a technical number/string. You should not attach marketing aspects or emotions to it. Avoiding a version bump due to spiritual or esoteric reasons can be done, but is not subject to a technical contract as SemVer is.

Version bumps are not there to be avoided. A vendor should signal change-events in this code base to its consumers as fast as they occur. In the very next release of your code base.

If a vendor collects many changes (e.g. more then one breaking change or just a couple) then the migration path / implementation burden on the consumer will become exponential, as the consumer will have to handle with more then one issue at the same time (complexity). This will result in "delayed migrations" and even in consumers that stop consuming this vendor package. This is one of the causes for "stuck for years" packages. Unmaintained consumers are one possible outcome. This vendor would harm the ecosystem.

@bartlettroscoe
Copy link

@RobSmyth

Software package releases should post their the package-specific version (i.e. Romantic Version or Calendar Version) as well as a mandatory semantic version.

Nice thinking. Very interesting. But I do not quite see it :-)

You say "package-specific version" and "mandatory semantic version". Who is the user for each?

  • The "user" for the "package-specific version" is for humans. (Likely better to call that the "human version")
  • The "user" for the semantic version is for package managers

The idea that 2.0 is a major upgrade over 1.0 is so ingrained in the public psyche that it is used for almost everything like "this is version 2.0 of Jeff, a major upgrade!". So we can't get away from the usefulness of that for communicating with humans. Yet, from a package management perspective, that is worthless information. A package manager only cares about set of backward-compatible versions.

If I guess that "package specific version" is a product version like "Visual Studio 2022" then would that not be the name and not the version?

That would be the human version. The human version can be any set of ASCII characters in any arrangement that the project would like to chose as a way to communicate with its human customers.

Even that would seem odd to me as it is not clear why I would want to have 2 versions in the one name. If the end user expects "2024.8.1" why add a semver?

Because version "2024.8.1" is not a sematic version and says nothing about the backward compatibly relationship between that version and say "2024.1.1". That type of version naming is completely worthless for a package manager.

But that comes back to the "mandatory" word.

If you package lives on an island with no upstream or downstream dependencies, then a package would not need a semver. (But then a package with no downstream dependencies, by definition has no users, and therefore is not even worth discussing. But such packages do exist, i.e. those that people write for themselves.)

Anyone who argues against semver is never tried to upgrade a bunch of dependent packages over many years.

All I am suggesting is a way out of this mess. Let people select whatever version naming/numbering they want to human marketing purposes (i.e. the "human version"), but get serious and provide a semver as well so that we give package managers what they need to try to upgrade package ecosystems in a sane and scalable way. The alternative is chaos (which is the de facto state of affairs).

@bartlettroscoe
Copy link

How to end this debate once and for all? => Have packages define two different version names:

  • Human/romantic version
  • Semantic version (semver)

That gives everyone what they want and we end this endless debate. You advertise the human version to humans but you also provide the semver so that package managers can do their job. Win win.

After 10+ years of listening to this debate, this solution seems so obvious that it is shocking that the software community has not already adopted this.

@robnagler
Copy link

@bartlettroscoe writes:

Anyone who argues against semver is never tried to upgrade a bunch of dependent packages over many years.

If it were possible to implement semver by fiat, then I would agree with semver. From my perspective (one who has maintained numerous software packages over many decades), version numbers are meaningless. I am often caught flat footed by (often capricious) dependent package changes on arbitrary version boundaries. AFAICT, the only thing enforceable by any package manager is that version numbers increase over time.

@christian-weiss writes:

Holding back changes can be done but nowerdays the industry (agile movement, extreme programming) tend to release often and in small chunks, as it helps to get faster feedback and to make it easyer to spot and fix issues faster.

That is not a version schema issue at all. You will experience pain with big releases independent from SemVer.

I believe semver defines this clearly: "MINOR version when you add functionality in a backward compatible manner". MAJOR versions are the only way to release backward incompatible changes. The implication of MAJOR is that they are either infrequent and/or all MAJOR versions are kept around and maintained forever. This is the promise of semver. This is not the reality of package management. With the "move fast and break things" culture, software maintenance includes validating breaking changes, constantly, which can consume a small software team maintaining sophisticated systems.

Which begs the question: If Linux, C, Fortran, Make, and a host of other software packages have maintained backwards compatibility over decades, why is it necessary to release breaking changes at all? If you don't like the way an API works, create a new one with a new name. Or, perhaps, add a new parameter with a logical default so that existing clients continue to work the old way. Add version numbers to configuration, protocols, files, etc. to maintain backwards compatibility.

IPv6 works 100% compatibly with IPv4. Sure, they could have broken the Internet with the major version number change, but they chose not to, and I'm sure you are all grateful for the thoughtfulness for users. Containerization is only possible, because the Linux kernel is 100% backwards compatible. Again, I am sure you are all grateful you can run software applications built with any distro on any Linux system, and even Windows and Mac.

This issue is cultural imho. As an example, there is a striking difference between Perl and Python. Perl strived (no longer use it so dunno) to maintain backwards compatibility. I have Perl programs (and users :), which still run after 35 years. Python strives for "perfection", which results in removing APIs and/or changing them. People could certainly argue Python "won" and "perfection" is the way. I think software maintenance now involves unnecessary friction.

@bartlettroscoe
Copy link

Anyone who argues against semver is never tried to upgrade a bunch of dependent packages over many years.

If it were possible to implement semver by fiat, then I would agree with semver. From my perspective (one who has maintained numerous software packages over many decades), version numbers are meaningless. I am often caught flat footed by (often capricious) dependent package changes on arbitrary version boundaries. AFAICT, the only thing enforceable by any package manager is that version numbers increase over time.

@robnagler, just because a given standard is not universally adopted or fully followed by those that attempt to implement it does not mean it is not worthwhile. You could argue that if a package manager ran the native test suite for each package in the package dependency graph, then it is the package manger that would be validating the semantic version numbers. For example, if a package posts a new release X.(Y+1).0 that it can detect is not backward compatible with X.Y.Z w.r.t. downstream packages, then it should be marked as such in that package manger. When the package manager points out that the release X.(Y+1).0 that is not backward compatible, then the team could decide to put out the patch release X.(Y+2).0 that restores backward compatibility and the package manger can mark X.(Y+1).0 out as non-backward compatible. See What do I do if I accidentally release a backward incompatible change as a minor version?. I know there a lot challenges in running the native package test suites by a package manger but that is where this needs to go.

@lolmaus
Copy link

lolmaus commented Aug 5, 2024

A SemVer version number is just a technical number/string. You should not attach marketing aspects or emotions to it. Avoiding a version bump due to spiritual or esoteric reasons can be done, but is not subject to a technical contract as SemVer is.

@christian-weiss I totally agree, and this is exactly why we need a non-technical contract. A version number to indicate package/app generation to humans, leaving SemVer to machines where it should be.

@robnagler
Copy link

@bartlettroscoe we are talking past each other, just as we have done before. Neither approach can be implemented by fiat. A key difference is this: every package that ensures backwards compatibility improves downstream reliability, because dependents can upgrade without thinking about compatibility. With semver, there is no such guarantee except for minor releases. With backwards compatibility, software becomes more reliable in general, because upgrades to the "latest and greatest" are seamless and there is less friction so there is more time to maintain the dependent software.

The cost of backwards incompatibility is immense, because it is NM where N is the number of packages and M is the number of dependents. The cost of backwards compatibility is linear: N, only the packages themselves need to be maintained. Not to mention that M is much larger than N so the difference between N and NM is more than quadratic.

@mindplay-dk
Copy link

A key difference is this: every package that ensures backwards compatibility improves downstream reliability, because dependents can upgrade without thinking about compatibility.

💯

With semver, there is no such guarantee except for minor releases.

You mean minor and patch releases, right?

and what's your point? I mean, with SemVer, only major releases (are supposed to) contain breaking changes, that's the standard - we need some way to signal a breaking change, right? Otherwise semantic constraints in package manager requirements wouldn't be any use at all.

With backwards compatibility, software becomes more reliable in general, because upgrades to the "latest and greatest" are seamless and there is less friction so there is more time to maintain the dependent software.

of course backwards compatibility is always preferable, whenever it's practical and realistic - in some cases though, things can be simplified and made more reliable by removing code that exists to supports backwards compatibility. Code like this doesn't typically make software more reliable - usually the opposite - so we want to remove it eventually.

I've been following this conversation for a long time, and I'm a bit confused. 😅

is this a discussion about SemVer or about change management in general?

if it's about SemVer, yeah, I agree, there are some problems with SemVer as described - but there aren't any major problems with SemVer as implemented in package managers, is there?

sure, sometimes you get a bad release, because people tagged it with the wrong version number - but I'd say that's maybe 10% of the time, absolute worst case, which means 90% of the time it's saving us a lot of work.

if I notice a package using romantic versioning, usually I just change my constraint to something like 1.2.3 and manually update that package as needed - it's something I've rarely needed though, as most packages (based on my experience with NPM and Composer) are generally versioned according to package manager recommendation.

speaking of, I will mention this again, since no one ever commented:

https://simversion.github.io/

it's a subset of SemVer, which references how version numbers are interpreted by package managers - which is much simpler and easier to describe than actual SemVer, which just seems to create confusion and start endless debates.

my hope with this was that developers would be more interested in solving problems than debating the complex semantics of a specification that (lets face it) most developers don't even bother reading.

it was just a first draft/pitch, but no one ever showed any interest.

wouldn't it be more interesting so solve the problem than to debate the pros and cons of the SemVer spec?

extract "the good parts" and give people something they can actually understand and apply in a way that makes sense with existing package managers? 90% of the value with 10% of the complexity? 🙃

@bartlettroscoe
Copy link

The cost of backwards incompatibility is immense, because it is NM where N is the number of packages and M is the number of dependents. The cost of backwards compatibility is linear: N, only the packages themselves need to be maintained. Not to mention that M is much larger than N so the difference between N and NM is more than quadratic.

@robnagler, the cost calculations are not that straightforward. Maintaining backward compatibility for long periods of time over a lot of development and many releases can become a huge drain on productivity of the package development team, especially for faster moving packages and those that are driven by research and large underlying technology changes. (I come from the area of computational science where changes in GPU and other accelerator technologies are massively disruptive and mandate breaks in backward compatibility in many cases.) One can argue that maintaining backward compatibility for old customers is a large tax on new customers that want to adopt and fund future development of the software.

If you push too hard for never breaking backward compatibility, then you force many teams to abandon packages and start from scratch with a new package for new costumers. Then someone has to maintain the old package and well as the new package. This reminds me of:

(See my summary of this views 20 years later here).

We have to look at the total area under the curve of productivity of package developers, downstream package developers, package ecosystem management, and end customers. For some packages, the minimum area under the curve will come from some key highly used packages needing to never break backward compatibility. But for many other packages with a smaller number of direct downstream customers, the area under the curve will be minimized if the package development team can dump the cost of backward compatibility at regular intervals and have the downstream customers absorb those costs incrementally.

In the organization where I am working on, they are trying to remove an old highly used software package that as not been activity developed for 15 years and trying to get customers to move to the new package that was ready at least 10 years ago. The old package was officially deprecated at least 8 years and is not slated to be removed until the end of next year! That process is hugely expensive for everyone involved.
One can argue that it would have been better and overall cheaper to just incrementally refactor that old package into the new package starting 20 years ago and slowly broke backward compatibility in smaller, easier to absorb, increments.

And that is why we need semver and we need to take it seriously.

@just6979
Copy link

So many comments just reiterating what SemVer is supposed to be, or how to do the bumps, or redefining RomVer, or suggesting different versions for humans and machines (what?). It seems lots of people didn't read the entire essay. We know what SemVer is supposed to do, the point of the essay is that what it's supposed to do is almost fundamentally imcompatible with Actual Real Life development. As stated in the essay, trying to lock breaking changes behind MAJOR has issues, as does trying to limit MINOR to only non-breaking changes. I saw few comments asking "Why does the percent matter when considering how many users it effects?", and I think those askers missed the whole point. If you do major bump for a breaking change that only effects 1%, then you've now imposed a large piece of work for the other 99%. Because users will need to grab the new major-bumped version to stay up-to-date, but also are now required to figure out if the alleged breaking changes actually break their stuff. If you do only a minor-bump for what you think it a 1% breaking change, you risk breaking up to 100% of users' stuff, because breaking change are still breaking changes and you can't know how everyone is using it.

It's basically all wrapped up in the final paragraphs: SemVer can help present logical progression of a project, but blindly following it, both on the creation side and the using side, increases friction. SemVer can be useful to present an overview of project progress, but reading detailed changelogs and behavior testing after upgrading are the truest ways to check if a change is actually breaking for your specific use case.

Yet another tl;dr: SemVer is not perfect, but if a project uses it, you're probably OK on both sides semi-blindly making and accepting patch-level bumps for bugfixes and security. But minor and major bumps should be given more consideration than just "is it breaking change or not?" on the producer side, and the consumer side should always check the changelogs and run all your behavior tests before even thinking about pushing to production. However, if as a producer you find youself trying to follow SemVer but end up with a bunch of major bumps and only ever have .0 or .1 minor versions, consider explcitly moving to something like RomVer to better track the project's actual progress.

@mindplay-dk
Copy link

@just6979 RomVer uses the second version number for breaking changes - besides being extremely confusing (as though you're intentionally trying to mislead users into thinking breaking changes are minor) this is fundamentally incompatible with every tool in existence. 😌

@just6979
Copy link

Tools don't know anything about breaking changes, that whole statement makes no sense. Because it is, again, missing the point that "breaking changes", as indicated by SemVer's major versions, isn't a known quantity, for eithertools or users.

Yes, RomVer uses the second number for potentially breaking changes for maybe some users, while SemVer uses the first number for the same. What RomVer adds is that the first number indicates there are very probably breaking changes for almost everyone because the changes are big enough that it would end up as a different project if operating under the limits of SemVer. The idea is to reduce friction around making those big changes that would otherwise end up in different projects, like apache-2.

This wouldn't break any tools that deal with versioning, they just follow the version specifiers that you provide. It very might require some changes in those version specifier conventions, since many many people use specifiers that under SemVer would try to limit updates to hopefully non-breaking minor bumps. But as OP stated, it's not a garauntee of avoiding breaks (behavioral changes still often sneak through even if the exposed API doesn't), while also giving a false sense that you'll be getting all the security and bug fixes that come with patch level bumps, even though active production may have already moved on to a new major version.

I'm not saying RomVer is the answer, just that it could be something to consider for a project to choose to both reduce friction around making big breaking changes, as well as helping the users better understand when breaking changes might happen.

@mindplay-dk
Copy link

Tools don't know anything about breaking changes, that whole statement makes no sense. Because it is, again, missing the point that "breaking changes", as indicated by SemVer's major versions, isn't a known quantity, for eithertools or users.
[...]
This wouldn't break any tools that deal with versioning, they just follow the version specifiers that you provide.

I have no idea what you mean.

Look at any package.json or composer.json etc. - they all predominantly use the ^ version constraint.

These are documented, idiomatic features of most package managers, directly relying on semver-style version numbers:

https://docs.npmjs.com/about-semantic-versioning

https://getcomposer.org/doc/articles/versions.md#stability-constraints

I'm not saying RomVer is the answer, just that it could be something to consider for a project to choose to both reduce friction around making big breaking changes, as well as helping the users better understand when breaking changes might happen.

The tools wouldn't understand. Worse, they would misunderstand - major.minor.patch has a defined meaning already.

You can't just say "the version numbers of my package have a different meaning", when the file you're writing for the package manager, and the platform you're publishing on, has a different semantic for the property/value you're using.

I mean, you can, but it wouldn't work - you would just break everyone's projects and they would be really annoyed.

You can use RomVer (or whatever you want) for the logical version number of your product, sure.

But for libraries, this directly conflicts with version numbers, which, like it or not, have a defined semantic, purpose, conventions, and tool support. I mean, no one can stop you, do whatever you want, but you will only create more problems.

@krainboltgreene
Copy link

My daughter went from elementary school to graduating from high school since this gist was created. Tilt at better windmills, yall.

@just6979
Copy link

Those documented idiomatic "features" are just convention. Those conventions assume everything out there uses SemVer, and uses it correctly (even though the OP's point is that determining breaking vs non-breaking is not always an easy thing).

The tools don't know that minor bumps are supposed to be non-breaking, you tell them to freely update to what you think/hope will be non-breaking by specifying the version constraints. The tools don't misunderstand, they just do what you tell them to. But the users might, and that's kind of the point. Doing perfect SemVer, ie: never ever breaking anything on minor (or patch) bumps; managing to get important fixes that only exist behind a major bump out to users since so many, through convention, have specified to never take a major bump, etc.

Making the assumptions that SemVer insists on is stifling on both ends. Minor bumps do break things sometimes, even patch bumps sometimes change expected behaviors and break things, so users need to be more careful than just "lock the major, take all minor and patch bumps", and devs need to do so much extra work: in deciding whether to save things up for a big major bump, whether to shoehorn improvments back to previous majors, when to stop supporting a previous major or minor.

Many many packages already do say "the version numbers of my package have a different meaning". Linux kernel, anthing with CalVer, basically all modern browsers (when's the last time Chrome or Firefox didn't have a .0 minor?) Some Python libraries effectively only bump the major and patch, either because breaking changes are either super-rare or practically unavoidable and users are expect to read the docs. Some packages basically always have minor version locked in real world usage because they're unreliable about not breaking things. Yes, changing it on the fly might cause issues, but it can be done, maybe after a major bump, after all that's what it's for!

And we did't even get into the reproducibility argument yet, and that no one should be leaving even patch level updates to be applied on the fly at deploy time. Which comes back to OP: set aside time to do upgrades on a regular basis, read the docs for stuf you use, bump versions according to how those rules apply to your usage, test thoroughly, be prepared to have to make fixes no matter the size/type of the version bump, then lock those versions for consistent behavior throughout the workflow.

@mindplay-dk
Copy link

I can only speak for myself, but if there's any chance a new release could break something, I document it and give it a major version bump.

I worked in one setting where we built a very modular system - many versioned packages with many dependencies. We used the same policy of liberally increasing the major version number, even for conceptual (not technically) breaking changes, even for bug fixes when those were technically breaking changes, and while this frequently led to what we called "the dependency dance" (upgrading a slew of modules) this saved us tons of time for during the many minor and patch releases.

I worked in that setting for 5-6 years, and I can't recall anyone every releasing a breaking change with a minor bump.

So I reject the idea that it can't work.

Although I do agree that people suck at this.

I think a large part of the problem is the intuitive resistance to a major version bump, "it's just a bug fix, yeah it's breaking, but it doesn't feel major" - an intuition that was likely fostered by the many years or romantic versioning that software grew up with.

We did have to stamp it into the heads of the entire team to get them to strictly follow SemVer - but it worked.

Breaking or non-breaking is not a "think/hope" situation - it's a matter of diffing before you release, paying attention to any public interfaces that may have changed, and then accepting the fact that even a bug fix or conceptual change can be breaking, and bumping the major version number liberally.

It will feel wrong until you realize that it works, and it works the way it was intended. 😊

@just6979
Copy link

Yes, following SemVer to the letter is possible, and arguably (you just showed it) not difficult. The question was more about if those liberal major bumps for breaking* changes but that might only effect 1% of users, are worth inflicting the "dependency dance" on the other 99%. Sticking to perfect SemVer means you're choosing to do that.

Also remember that the need/want/requirement for major version bumps to "feel important" doesn't always come from the developers. It sometimes comes from management. And it can also come from user intertia: it's hard to justify spending the time on a "dependency dance" when the end result is a conceptual change in a part of a library that I don't even use, so that major bump might just get skipped until some free time pops up (hahaha!) or an actual security of bug fix is included.

Yes, SemVer works as advertised re: messaging around breaking vs non-breaking changes. But is it always worth the costs it can sometimes incur on users?

  • (no need to say "technically" breaking for bugfixes if the rule is "a breaking change is breaking change no matter how big or small")

@lolmaus
Copy link

lolmaus commented Jun 18, 2025

I worked in that setting for 5-6 years, and I can't recall anyone every releasing a breaking change with a minor bump.

And you haven't slipped a single bug in half a decade? 😬

And are all of your dependencies and subdependencies following SemVer to the letter, without exceptions? And there hasn't been a single bug in them?

@oisin
Copy link

oisin commented Jun 18, 2025

This has been going for a while hasn't it. As a consumer of open source code, and a producer of open source and closed source code, I depend on SemVer being exactly what it says. There's no concept of "perfect SemVer", it's SemVer or not Semver.

The question was more about if those liberal major bumps for breaking* changes but that might only effect 1% of users

You can never tell for sure, unless you are tracking, what the size of the blast radius will be. Using SemVer is a clear message to everyone what is going on: there's a breaking change, be careful here.

But is it always worth the costs it can sometimes incur on users?

That is entirely up to the user to decide. Take the cost, or keep the current version. If I update a thing, it's up to the consumers of the thing to make the call on the upgrade, not my call as a producer of the software package/item.

@lolmaus
Copy link

lolmaus commented Jun 18, 2025

There's no concept of "perfect SemVer", it's SemVer or not Semver.

Exactly.

But since virtually every library has (sub)dependencies that violate SemVer, every library has bugs, every library has (sub)depenencies that have bugs — it's never SemVer.

@oisin
Copy link

oisin commented Jun 18, 2025

virtually every library has (sub)dependencies that violate SemVer

I'm going to see some real study data on that particular claim before I can accept it as truth, or even partial truth.

@lolmaus
Copy link

lolmaus commented Jun 18, 2025

virtually every library has (sub)dependencies that violate SemVer

I'm going to see some real study data on that particular claim before I can accept it as truth, or even partial truth.

Here are some projects that don't follow SemVer and that your projects may depend on:

  • Linux kernel: has breaking changes in patch versions
  • Node: dropping platform support not always treated as a major version
  • Rails: does not follow SemVer, with versioning not reflecting compatibility changes
  • Python: officially not SemVer, minor releases can have breaking changes, especially in less-used features
  • Ruby: officially not SemVer
  • jQuery: has had breaking changes in minor updates
  • npm: has had breaking changes in minor updates
  • TypeScript: uses decimal-like notation: 3.9 → 4.0; never 3.10, minor version bumps can break types
  • Express: had unexpected middleware behavior changes in minor updates
  • ESLint: a minor version bump can cause ESLint to report more errors and break your CI build
  • Next.js: head breaking changes in minor bumps more than once
  • Cryptography (Python) :switched build system to Rust in a minor update (e.g., 3.4.0), breaking builds

This list can go on forever, virtually no project is a SemVer saint. And as you said, if it's not strictly SemVer, it's not SemVer at all.

And every bug is a SemVer violation! 🤯 Do you need "real study data" that bugs happen?

Another big issue is that there can be implicit dependencies on internal behavior of a library. E. g. I use a library in my app, it works. I upgrade the library's minor version and my app breaks. When I investigate, I find that some internal behavior of the library has changed. I confront developers about it — and they respond that it was private API that changed and they never promised it to be stable. Some race condition started to have a different outcome after the upgrade, or something like that, the specific behavior was not even documented, so library maintainers felt not responsible. Note that I wasn't even using any private APIs! I just used the library and it happened to have certain behavior which changed in a subtle way without a major bump, breaking my app but not violating SemVer because they didn't change any API footprints!

Believing that three numbers can protect your app from regressions caused by upgrades is just... ridiculously naive. I can't wrap my head around how many people smarter than me believe in this fallacy. It feels like SemVer is a religion that defies reason and critical thinking.

Every dependency upgrade must rely on an extensive test suite and occasional manual QA. In web development, if you do minor version bumps without at least reading changelogs — you are doomed to have occasional regressions. I web developer who hasn't had a single one in 5-6 years is a happy developer. I envy you.

@oisin
Copy link

oisin commented Jun 18, 2025

Thanks for putting together that list! If those projects don't follow SemVer, but claim to, then that's definitely a badly-behaved project in that regard.

And every bug is a SemVer violation! 🤯 Do you need "real study data" that bugs happen?

I don't get the reasoning that a bug is SemVer violation. Is it because a bug is unexpected behaviour that can break your stuff?

I confront developers about it — and they respond that it was private API that changed and they never promised it to be stable.

That's on them--they are bad actors and are untrustworthy or unserious developers, imo. It's not helpful for us, the consumers, I know, but it's a message that you might need to change your risk assessment on using that software. Ok, it's funny to say that the Linux kernel developers are unserious, but if they have committed to SemVer, claim to practice it, and then do not, well then that's not a good look. (I don't know if they claim it, just picking the top of the list there. If no-one claims SemVer, then all your bets are off on versioning).

Believing that three numbers can protect your app from regressions caused by upgrades is just... ridiculously naive.

No-one believes that. We would like it if the people that produce software that has versioning and claim SemVer actually took their job seriously, but as you pointed out this is just not true. We would love teams/projects to be committed to their customers, but not all of them are. There's no naivety here, a version number doesn't protect you, as any software developer knows. That is why we have tests in place to check for subtle changes in behaviour, why we do incremental rollouts, etc.

The SemVer claim doesn't guarantee anything above and beyond whether you can trust the project developers to be true to their stated claims.

SemVer is not a religious thing at all, it's a behavioural commitment that a project gives to its consumers. Breaking that commitment, silently and with warning is a super dick move from a project.

@lolmaus
Copy link

lolmaus commented Jun 19, 2025

If those projects don't follow SemVer, but claim to, then that's definitely a badly-behaved project in that regard.

Half of them claim to follow SemVer and have violated it. Others officially don't follow SemVer, but numerous libraries that depend on them —do.


I don't get the reasoning that a bug is SemVer violation. Is it because a bug is unexpected behaviour that can break your stuff?

A bug in a dependency can break your app. A dependency version that has introduced a bug is literally a breaking change. Bugs often appear in patch and minor versions, breaking apps all the time, routinely violating SemVer.


A common understanding of SemVer is that you can safely upgrade minor versions of dependencies, since they don't contain breaking changes. But a minor upgrade can break your app in many ways:

  • A bug in the upgraded dependency or its subdependency.
  • A SemVer violation in the upgraded dependency or its subdependency.
  • A behavior change in the upgraded dependency or its subdependency, that is not technically violating SemVer because it's private API or under-the-hood logic that was never promised to be stable, but your app implicitly depends on it in a subtle way. You wouldn't even know about it until it breaks on a minor upgrade.

That's on them--they are bad actors and are untrustworthy or unserious developers, imo.

Let's not focus on "on whom it is" for a moment. What's important here is that all software development depends on projects that either don't follow SemVer or have been violating it. According to your definition of SemVer («There's no concept of "perfect SemVer", it's SemVer or not Semver»), nothing is SemVer.

If the SemVer spec itself had minor releases, it would be possible to find an itty-bitty nitpicking breaking behavior that would affect literally nobody but would still technically be a breaking change, making SemVer not SemVer.


The SemVer claim doesn't guarantee anything above and beyond whether you can trust the project developers to be true to their stated claims.

And the entire tree of their dependencies and subdependencies! That they don't have any control over and that they cannot realistically avoid using.

Breaking that commitment, silently and with warning is a super dick move from a project.

Looks like the only way of not being a dickhead for a software project is to announce that they're not attempting to follow SemVer at all. Which many projects do, and maybe partly for this very reason.


PS I'm not saying that I'm against the cause of SemVer. I wish the software development world worked like that, and developers' lives would be so much easier. But it's not and they're not.

@oisin
Copy link

oisin commented Jun 19, 2025

I've just realized something, from the bug-breaks-app position stated above, that we are are looking at slight different aspect of applications of SemVer. I think I am applying it to library-level behaviours, individual packages eg if I am releasing a ruby gem, I'll check the behaviours before I ship it, making sure there's no breaking changes to the library as best I can, and definitely ensure that the API is scanned for no breaking changes. I mean, if you are a software provider you check as best you can that you are aligning with your commitment to your customers. So - I don't align with SemVer to an app, but I do align with SemVer on libraries that compose an app, if that makes sense.

I guess I have spent long enough differentiating between the "marketing" version of a thing, the "release" version of a thing, and any contractual guarantees that have been inked (for proprietary software) based on upgrade impacts and how to locally enforce them at the dev level. I've also worked with "an API is forever" organizations, where you have to keep everything moving along until people stop using the 0.2.1 API that was released 8 years previously.

Developers lives are not easier and are not getting easier and it's a pain in the neck, but now I have gone entirely off-topic. Thanks for the conversation 👍

@robnagler
Copy link

@oisin writes:

Developers lives are not easier and are not getting easier and it's a pain in the neck, but now I have gone entirely off-topic.

I like to think about why developers lives are not getting easier. I think by choosing to "move fast and break things", we are causing our own headaches. Consider how smoothly developers survive major versions of the Linux kernel, make, gcc, libc, and other tools, libraries, and languages that "don't break userspace". I can run a 40 year old Makefile today to compile a program which will run correctly (see my 2015 article Major Release Syndrome).

SemVer seems to give us a license to ignore backward and forward compatibility.

@mindplay-dk
Copy link

And you haven't slipped a single bug in half a decade? 😬

Of course.

If we accidentally shipped a breaking change as minor, a patch release would be issued, reverting that change.

Then a major release would be shipped.

And are all of your dependencies and subdependencies following SemVer to the letter, without exceptions?

We can't control what third-party dependencies do - but for some reason, most of the third-party Composer (PHP) packages we used were very well versioned. NPM for some reason is much worse about this. It could be cultural, in part, but I suspect maybe a large factor is PHP has type-hints. (Not sure if the situation is better for TypeScript packages? I haven't really thought about it before.)

And there hasn't been a single bug in them?

Everyone ships bugs, I'm not claiming we did it perfectly - we took versioning seriously, that's all. 😊

There's no concept of "perfect SemVer", it's SemVer or not Semver.

That is the hard line stance we took as well.

But is it always worth the costs it can sometimes incur on users?

That is entirely up to the user to decide. Take the cost, or keep the current version. If I update a thing, it's up to the consumers of the thing to make the call on the upgrade, not my call as a producer of the software package/item.

Exactly this.

By liberally increasing the major version whenever it seems prudent, your users get to make a conscious and deliberate choice - we also maintained a mandatory change log explaining any breaking changes, why they were made, and how to upgrade.

every bug is a SemVer violation! 🤯

Not strictly wrong.

But of course, the intention with SemVer is to document/specify with version numbers what you know.

Bugs aren't normally known at the time of release, and so of course no one can be expected to get this right 100% of the time.

What we can do, is roll back a breaking change that we versioned as a compatible change, and correct the mistake - every bug is a SemVer violation, but one that you've committed to correcting with another release to restore SemVer compatibility.

I confront developers about it — and they respond that it was private API that changed and they never promised it to be stable.

For internal members, we used the @internal annotation - calling an internal constructor (or method) will highlight with a warning in most IDEs. Users should understand what this means - if you're using something that's internal to the package, it's subject to refactoring, and there are no guarantees or commitment that it'll even exist in the next release. We taught our team to do this.

SemVer is not a religious thing at all, it's a behavioural commitment that a project gives to its consumers.

💯

@pauldraper
Copy link

pauldraper commented Jun 19, 2025

every bug is a SemVer violation! 🤯

They are GENERAL API specification violation, regardless of what versioning you use.

When that happens, the appropriate response to fix the bug in a new patch release.

Thankfully, SemVer helps us communicate that! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment