Skip to content

Instantly share code, notes, and snippets.

@jashkenas
Last active November 29, 2023 14:49
Star You must be signed in to star a gist
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)

@superarts
Copy link

SemVer may not only be good for robots; for library it has its place. But for end-users, as author said it may provide misleading information, thus I wouldn't use it.

@iamchriswick
Copy link

iamchriswick commented Sep 6, 2017

SemVer and "Semantic" Versioning is useless when it comes to developing Google Chrome extensions. As stated in the developer docs ,the version of an extension must be identified by one to four dot-separated integers and they must be between 0 and 65535, inclusive, and non-zero integers can't start with 0.

As an ending note, yes @thomasdavis, "romantic versioning" is freakin' funny :D

@aaronbeall
Copy link

I agree, "any breaking change" can make even the smallest change bump the major version, which puts it in the same league as a complete rewrite which just doesn't make sense.
As for the idea of automating this, Elm does that: https://github.com/elm-lang/elm-package#version-rules
Of course it helps that they have a 100% statically typed language to determine if surface area changed. JS will never really be that way. Maybe TS projects could employ an automated versioning mechanism like Elm, though?

@jedwards1211
Copy link

@aaronbeall not just TS projects -- I'm sure it would be possible to use some type of comments and/or Flow type checks to automatically detect changes in the API surface as well.

@jedwards1211
Copy link

@jashkenas Semver never "promised" anything and it certainly isn't a perfect system, but it's a better system for dependency management than a lot of package systems that came before it.

Anyone knows that even a patch change can accidentally introduce a bug that completely breaks existing API. No one's dumb enough to think that Semver was designed to prevent that.

A lot of packages I've used periodically have major releases with breaking changes that don't affect me. It's a bit annoying spending the extra time reading the changelog, but hey, at least the major version number change got me to look at what changed instead of just installing the new version and assuming I'll be okay if a cursory smoke test doesn't reveal any initial issues.

the responsible way is to set aside five or ten minutes, every once in a while, to go through and update your dependencies

Five or ten minutes is reasonable for a small-scale project. But for a large-scale project with hundreds of dependencies, you're talking about an entire day or more to review a lot of updates. Especially when you consider the potentially thousands of transitive dependencies that go into something. Sure it is disconcerting that I don't know all of those transitive dependencies personally and that even a patch change to a transitive dependency I'm not even aware of cause me serious problems. But you know what? I'll deal with that when it comes up. Most high-quality open-source packages are very responsible about adhering to Semver.

And the other responsible way to manage a project is to write thorough tests, and the better the tests are, the more you can rely on them to detect problems with automatic package updates.

It's pretty clear that what you wrote isn't actually forthcoming with the root causes of your frustration with Semver. What is it, really? Were you wanting to release small breaking changes to Backbone without frightening users with a major version increment, and then getting complaints from those who suffered from the breaking changes that Backbone didn't follow Semver? Where does this desire to get a "feeling" of how far away two versions are come from? (I never really care how far away two versions are, I just care if there's been a breaking change.)

@semanino
Copy link

semanino commented Feb 18, 2018

Completely disagree. Author (jashkenas) is inconsistent with himself.

SemVer helps to automatize exactly what he recommends, namley examining dependencies at regular intervals, see if there are newer versions, and update to these, adjusting own code where necessary.
SemVer is poised to do that very examination automatically, saying

  • where one can update definitively without any need to adjust code (PATCH and MINOR)
  • where an update will quite sure require adjustments (MAJOR)

So in fact the author, jashkenas, gives a really good recommendation for SemVer!

The fact that maybe something like node.js is currently under heavy development, and that things (i.e. interfaces) as a consequence change very often, is no excuse for bursting out with what one just tinkered with. I do not want to offend anybody with such a statement, but to create a mess and then blame a recommendation targeted at avoiding a mess isn't what I'd attribute fair.

A note on the mentioned semi-automated upgrade process of dependencies:
I deem it very likely that none of the whole bunch of great dependency management tools out there (there is for sure at least one for almost every programming language, sometimes multiple competitors) is capable of producing a consistent and comprehensive report saying "you should upgrade xy to minor version z" factoring in all transient dependencies.
If there's one, please tell me, I would be happy!

@dhwang
Copy link

dhwang commented Jul 25, 2018

Grr... Why Google search send me here? Feel like a waste of time mainly because there's lot of criticism without offering alternative or solutions or suggestions.

Semver is a scheme for machine (so?). Like all scheme you need to know what you sign up for.

@skyl
Copy link

skyl commented Aug 15, 2018

@dhwang Google sent you here because this is a classic post.

@EarthCitizen
Copy link

Semantic versioning requires teams to obey the contract. I have seen many cases where that is not happening, but that is not the fault of the semantic versioning. Also, this system does not work in all cases. I have seen a few oddball cases where semantic versioning is not a good fit.

@erickdi
Copy link

erickdi commented Jan 14, 2019

SemVer may not only be good for robots; for library it has its place

Breaking changes are in the works that will bump it to 2.0.0.
Printable calendar

@bogdanpetrea
Copy link

I didn't read all the comments in this thread, but it is not so uncommon for projects to use two different ways of versioning:

  • one that reflects the old way of versioning, so humans can guess how big of a change the product went through
  • and an internal semantic versioning, so developers can better understand how the codebase changed

@chindraba-work
Copy link

Thanks for the endorsement of SemVer. I was not too sure about it before reading this, add the nest of links and issues it leads to. Now I'm sure this is the system I will use on my projects. If I comply with the published specs, those who use my projects in any fashion will know all they need to know about each version I produce. If I make a mistake, as my wetware is known to do, then I can rightly expect my consumers to respond just like they did to the maintainer of underscore. I've also learned that the philosophy of "trust but verify" applies to SemVer just as much as it does to anything else with a promise.

@chindraba-work
Copy link

@Poikilos Unfortunately Firefox does need a Major bump for each, or almost each, release. Binary extensions which link to the core library of Firefox (libxul) have to be rebuild (and re tested) for each new version of Firefox. As such the version numbering, in that regard anyway, seems to follow SemVer. See Binary Interfaces

extensions that use binary components will need to recompile for each major version they wish to support.

@Kholdfyre
Copy link

You make some very valid points, and in most of my opinions about development, I'm old school (eg, I prefer Perl or C/C++ and even assembly language over Ruby or Rust--no offense to either; just my preferences--if something's not broke don't try to fix it) in most of programming preferences and opinions, having been coding since before most people even knew what the Internet was, almost nobody had a cellphone and you were still made fun of in school if you even knew how to turn on a computer. I grew up using BBS's, playing MUDs and coding in x86 assembly, Fortran or good old C89 on BSD and then Linux—and yes, DOS too and eventually Windows 3.1, although not CP/M. My first computer was actually a Commodore 64/128 lol. So I get what you mean about the humanity being removed from software development these days, similarly to how nobody wants to write actual desktop (or God forbid command-line/terminal) applications anymore—they try to turn everything into a damn smartphone app or "Webapp" [sic] with a "simple" design, which means featureless, no dialog boxes (everything opens in a Web-page-like view O.o), few configuration options and other weak design practices. In fact a lot of software isn't even developed or coded anymore. It's just put together like a puzzle. This has its advantages of course, for instance, reusability and sometimes code health and stability, but kind of like you said it takes the humanity out of development. We are not robots. I taught myself and was taught formally later to design, code, debug and configure a piece of software as a well-made, handsomely designed engine or a handcrafted piece of furniture, not on an assembly line using clumsily-fitting building blocks that weren't even intended to be used together (well this is hyperbole, but still xP).

My point is I agree with you for the most part. But I also was brought up to try and see both sides of every story (my step-dad was an LAPD officer and NOT easy to live with, but he taught me well and influenced my later decision to become a Marine) and see the good (and bad) in everything, since almost nothing in life is ever simply "black and white", but somewhere in between. So while 99% of your thoughts are also mine, and I'm glad I'm not the only "traditional" programmer out there, I do see some parts of semantic versioning and similar practices that could in some cases be advantageous.

Thank you very much for giving me another viewpoint, and letting me know I'm not alone. xP I couldn't find any other stuff that looked at semantic versioning as anything but perfection, and whenever I expand my development horizons or learn a new language I try and get a picture of the positive and negative points of its use, so you really helped me. :)

@Eneroth3
Copy link

Eneroth3 commented Jul 8, 2019

In the case of user applications I agree to some extent. A notable change to the UI may make sense to regard as a mayor change even if technically no compatibility was lost. However, if you think of the user as a program and user behavior as code, doing a notable UI change is a breaking change as the user needs to change their behavior.

For code used by other code, I think SemVer works rather brilliantly. Rather than focusing on whether a change broke the code for 1% or 10% or 100% of users, I'd say it is a matter of whether public API has changed in a breaking way. Did people rely on undocumented side effects, bugs or internal code? Then it is their problem if their code breaks and not a breaking change in the public API. Is there a breaking change in part of the public API that you happen to know not a single person is using (say you asked the 2 collogues with access to the library)? Still a breaking change! If you don't fully document your API and how it is intended to be used but expect people to figure it out themselves, then that is the problem, not Semver.

@Kholdfyre
Copy link

Well, I agree with most of your assertions, almost all, in fact. To be honest, I've never liked "enforced" versioning schemes. Software development is not only a science, practice and occupation, but an art and a craft, and each project is different. Even those made of "lego pieces" (e.g., components or modules pre-built) are essentially a piece of software architecture. And although many types of software may have a basic, common versioning scheme, to try to fit the myriad of different development methodologies, project management schemes, not to mention the personalities of each team member and his/her style all into a single, rigid versioning scheme is like trying to fit a cube, a cone, a cylinder and a circle into a circular hole. Sure, the circle and possible cone or cylinder might fit, but they won't all. :)

@thhevs
Copy link

thhevs commented Sep 24, 2019

I see one serious pitfall with SemVer. With ever increasing software release pace (and it's continuing to increase) we soon end up of having all minor version numbers will always be at 0. :) And I already see this effect in some famous software products (Firefox is a good example. Google Chrome is another one). This can (and actually will) finally lead to a total abandonment of all numbers after the first dot! (which is also already a case in some modern products). The question is - why do we need such versioning system? Does it have any real meaning, except just the continuous numbering, which says nothing about feature course changes, bug fixes or maybe full refactoring of the whole codebase, only because they all break the backward compatibility? By the way, is a full refactoring of the system, which doesn't brake backward compatibility just a minor change?! Why on Earth we must version our software only on merits of backward compatibility? Software versioning is much more than that.

@pbodnar
Copy link

pbodnar commented Sep 28, 2019

The article and discussion in short, with a little bit of my point of view:

  1. SemVer as such is not bad, you just don't need to follow it strictly. It also depends on what type of stuff you deliver.
  2. Changelog is a must no matter what versioning system you use. Because no versioning system will ever be perfect.
  3. In essence, major.minor.patch seems to be the sanest versioning schema in the long term, verified over time.
  4. The 3 numbers are meant for easier communication among people in the first place!

For me, everything else are "technical details", some are misunderstandings, some include argumentation fouls...

I rather wonder, but it's a question for a whole new discussion, why big players like Firefox, or seemingly Postgres lately, went nuts and decided to give up communicating the changes via a meaningful version to their users. Do you remember the jump from Firefox 56 to 57? And are you already looking forward to Firefox 100? ;) @thhevs, the fact that you release often, doesn't mean that you always need to increment just the first number, there is no mandatory implication between those...

@fredheidrich
Copy link

I see SamVer at least bringing some layered context to versioning: the versioned backwards compatibility against a public API. With the rants out of the way, what's considered better than SamVer by 2020 standards?

@pmonks
Copy link

pmonks commented Jul 18, 2020

I see SamVer at least bringing some layered context to versioning: the versioned backwards compatibility against a public API. With the rants out of the way, what's considered better than SamVer by 2020 standards?

Nailed it. I see a lot of these "SemVer is terrible" rants and for the most part find myself agreeing with them, but then they either neglect to discuss alternatives at all, or pretend that those alternatives are somehow immune from those very same issues that they lambast SemVer for.

To misquote Winston Churchill "SemVer is the worst form of version identification, except for all those other forms that have been tried from time to time". I'm open to the possibility that a better scheme exists, but have yet to see anyone describe such a thing in detail.

@twome
Copy link

twome commented Sep 2, 2020

Bad take.

@tzimpel
Copy link

tzimpel commented Sep 10, 2020

A common misunderstanding is that semantic versioning is meant to be used for software version numbers. It's not.

Semantic versioning is used for packages / libraries that have a public API that is consumed by other software (and used by their respective developers). And it was never intended to do anything else.

Only in the context of an API, SemVer has any meaning at all:

  • Bugfix version increase -> It fixes bugs. It does not add features. It does not break backwards compatibility in any way.
  • Minor version increase -> It might fix bugs. It does add new features or extend existing features (and the corresponding API), but still allows it to be used in the same way as before (but without the new functionality). It might deprecate functionality without actually removing it.
  • Major version bump -> It might fix bugs and/or add new features. Whatever it does, it breaks backwards compatibility with the last version in at least one aspect of the public API.

For everything besides APIs, either use romantic versioning, marketing version numbers, code names, or whatever you see fit. If you have a software product that offers an API to connect other systems to it next to a UI that is consumed by actual users, you might just as well use a marketing version number for your software, while using SemVer to version the API - I've actually seen quite a few products do just that.

@martins-1992
Copy link

martins-1992 commented Mar 19, 2021

IMHO:
Changing an API in an backwards incompatible way is something completely different to breaking someones code that has this dependency.
One can occur without the other and this relates to both.
Semantic Versioning does only state, when an backwards incompatible change is present and when there is a new feature, nothing else.

From my point of view, the author is mixing (planned/intended/documented) API changes and actual implementation changes.
I consider this something completely different.

I also do not see the point, regarding the resoning, that some projects do not use SemVer.
If a project in question does not use SemVer, than you do not interpret its versioning as SemVer.
To expect that a project's versioning represents SemVer, if it is not stated or when it is actively not complied to,
does not make any sense to me.

@kyleerhabor
Copy link

Rich Hickey, the creator of Clojure, talked about this in his spec-ulation presentation, which was created two years after this gist. He talks about similar issues with semantic versioning and the general way we convey change, stating that we should value accretion over breaking changes and properly manage location for when deprecation is necessary (from variables up to namespaces/modules).

I think it's an excellent talk that conveys the issues with semantic versioning in better detail and an alternative to it.

@pmonks
Copy link

pmonks commented Aug 16, 2021

@kyleerhabor as a Clojure developer myself, I reluctantly find myself scratching my head a bit with Rich Hickey's views in this area (despite also encouraging everyone to check out his presentations).

Yes it is self-evident that we should all aspire to only accrete, relax, and fix our libraries (and a majority of the time that's indeed entirely reasonable), but in my experience, in any non-trivial, long-lived codebase there almost always comes a time where something has to change in a way that will break consumers, and can't simply live on in perpetuity as a deprecated, but still functional, feature. Typically this is ultimately because something in the real world changed in a backwards incompatible way; a standard, a human process, a third party system, a business priority, legislation, etc. etc. ad nauseam.

Semantic versioning at least gives us a standardised way to identify different classes of change (even if it's far from perfect).

@kyleerhabor
Copy link

@pmonks thanks for the response. Rich Hickey's presentation is one of his most controversial, some of it I even find confusing. Often, the real world and other dependants get in the way of the "accretion without breakage" philosophy. Rich doesn't talk about it in his presentation, but he does mention other examples with solutions.

For example, if your software is not used by a third party (e.g. a client hitting the endpoint), you don't have to apply any of Rich's principles since you're free to modify in a fixed environment. However, if it is, it depends on how it's used. Are you running a public web server, or providing a public library? You can still apply the principles in both environments, but there are some cases where deletion is the only option (last case scenario).

Software should be designed to support these changes by not coupling too tightly. If you hit POST /articles with a new article and a new field is required, a new endpoint POST /articles2 could be created while sharing logic in the presentation layer (GET articles/...). However, if an external force prohibits you from maintaining your stack, deprecation arrives. In serious cases, deletion arrives (e.g. you were storing something but now you can't). In a library, when a breaking change is necessary, deprecation or even a new namespace can be a solution. When the whole library needs to be refactored, a new library may satisfy.

In Datomic, for example, you have excision, which is only used when you absolutely have to delete data (e.g. the GDPR's "right to be forgotten"). But often, these measures are inefficient. Not only for performance, but in actual deletion of the data. If you delete the data but have a backup, you've failed to acknowledge the user's request to delete the data. One solution is crypto-shredding where the data is encrypted and the decryption key is deleted so the data is locked forever.

Semantic versioning is popular since it models the real world but the cost to consumers can be very detrimental. In my opinion, like the author, it's not a great way to identify classes of change. Like Rich said, with patch and minor, you don't care. With a major change, you're screwed. What do versions 1.5.0 and 2.0.0 convey? Is it a small change or a complete overhaul? Will it take Sam 10 seconds to fix and Bob a week? Changelogs are answers to that, but the version itself doesn't say much. Accretion over breakage is not immune to the real world but is one solution to the problem. It does work in many applications but has mixed feelings. HTML, Unix, and Java to name a few. It requires a lot of discipline.

@pmonks
Copy link

pmonks commented Aug 16, 2021

Thanks @kyleerhabor - I assure you I have a solid understanding of the concepts. The reality is that unavoidable breaking changes, however rare, do happen, and the only practical solution I know of in such cases is to communicate those breaking changes as clearly as possible, and ideally with as much lead time to downstream consumers as possible.

Which gets us back to my original point: while it is clearly far from perfect, SemVer at least provides a standardised, machine-consumable, ordered (both in the sense of "sorting", and in the sense of "disciplined") way to communicate breaking changes. Simply coming up with a new name for something that's broken backwards compatibility doesn't even have those benefits; it's a "wild west" / "do whatever you want" solution that ignores the needs of downstream consumers.

@kyleerhabor
Copy link

@pmonks thanks - I only watched the talk recently, so there are certainly people who have a much better understanding than me. Unavoidable breaking changes can happen, but it depends on what the project is. If a project depends on a service that prefers breaking changes (let's say an API), then sure—accretion over breakage is not sustainable. However, many projects can use it instead of semantic versioning.

Underscore is, in my opinion, a perfect example of where accretion without breakage would've worked. Instead of changing an existing function, it could've been marked as deprecated and suggest users use a newer function with a different definition. It would've been feasible since it's a utility library for writing functional JavaScript.

But not all libraries get such luxury. A library that chooses accretion without breakage relying on another library that instead uses semantic versioning is a bit difficult. Semantic versioning dominates the game in many ecosystems. Node.js follows it (except with odd numbers) and so does npm, making it the de facto choice.

But not all ecosystems are like that. In Clojure, it's a mix between unclear versioning semantics and accretion without breakage. It's machine-consumable, ordered (many libraries include the number of commits in a component), and just requires not deleting or changing the meaning of a function. In Rust, although it uses semantic versioning, the compiler strives to be completely backward compatible and introduces minor hacks to get around accidental breaking changes. A program I wrote in 2016 Rust will compile in 2021 and gets around it by introducing editions to make breaking changes.

So, yes, I do agree that accretion without breakage is difficult for some projects today. I don't think it's a one-size-fits-all like semantic versioning is. However, it's an alternative to semantic versioning. It means upgrading a library version without worrying about the breaking changes it could've introduced because there are none. It means minimizing backward-incompatible changes and being explicit about what necessary changes were made (did 2.0.0 change one function or 100 of them?). It's not just about libraries and applications—it's about producers, too. APIs are capable of applying their principles as well. Incompatible changes will happen since we live in a stateful universe, but like how Clojure offers features to properly manage the transition between states, we're offered an alternative to semantic versioning and responding to breaking changes.

@pbodnar
Copy link

pbodnar commented Sep 22, 2021

@pmonks, sometimes renaming a library is IMHO the only reasonable way - look for example at the story of Apache Commons Lang 3. I think that this is what @kyleerhabor meant when writing about introducing a "new namespace".

@pmonks
Copy link

pmonks commented Sep 22, 2021

@pbodnar sure, though their solution is driven more by Java's lack of version-aware modularity than anything wrong with SemVer. In fact it's interesting to observe that not only did the authors of Apache Commons roll the version number in accordance with SemVer, their workaround to the problem I mentioned in the previous sentence involved duplicating the new major version number into the Java package names. One might argue that they doubled down on SemVer.

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