Rich Hickey will tell you that breaking changes are horrible and versioning is stupid. The idea is nice. No breaking changes, ever. You get the API design of whatever you’re building perfectly at the first try. Oh wait. Obviously no one can do that, and no one could ever do that.
The question then becomes just how long exactly are you willing to carry the dead weight of code you don’t really want to carry anymore. Or rather even, how long exactly are you able to pay the costs of maintaining a possibly very problematic old API design.
I work on a system that has a large and old Rails application at its core. It heavily depends on features generally discouraged, such as single table inheritance. And upgrading Rails is hell. In Spec-ulation Rich Hickey keeps saying that (versioning is dumb and) maintainers will break you anyway. I can attest. Just a few weeks back I spent days fixing problems caused by a Rails patch version upgrade—that should definitely never happen!
My understanding of semantic versioning is simple. Patch: whatever. Minor: new features! Major: something might break. So why do I have to debug an absolutely breaking change in a Rails patch version about how scoped relations are handled? Used even by Github I’d like to assume that Rails has a thorough test suite. That there is a well-defined process for assessing changes. Except there doesn’t seem to be, and that’s the problem.
I might get grumpy about having to deal with breaking changes in a major version upgrade. I get mad about breaking changes in a minor version change (or, in the case of Rails, just let out a resigned sigh). I get absolutely furious about breaking changes in a patch version. Why even bother to pretend using semantic versioning if you then go ahead and ignore all the “semantics” of said versioning?
Of course versioning doesn’t stop at the API edges. Behavior can be broken too without any differences in the API. Static types won’t help you there, only a thorough test suite. My understanding of semantic versioning applies there too. Patch: whatever. Minor: something new may be added. Major: things may change significantly. I don’t mind reading the changelog of a major release to see if I should pay attention to some breaking change (though I’d definitely prefer deprecation warnings in a previous release first). Having to dig through commit logs to figure out why my multiple-insert queries are suddenly failing after a patch version upgrade, I do mind.
Sometimes breaking changes can’t be avoided. An API design decision might lead to security issues that cannot be resolved or avoided without breaking the API spec. An implementation detail might cause unstability that cannot be addressed without breaking some behavior assumptions. The API surface might be cumbersome to use and the maintainer can’t afford to backport every single change. I don’t think breaking change is necessarily a capital crime that should be avoided at all costs.
Communicate properly: disclose what changed and take measures that users won’t be broken by surprise. Semantic versioning is supposed to be a means of communication like that. It’s a whole different issue that maintainers give no fucks and introduce breaking changes in patch version. In my opinion that’s something package managers could implement and enforce, at least for the API surface.
Require packages to disclose an API spec. Validate that there are no “bad” changes whenever there is a release. Something like Swagger for library APIs. Something like Clojure’s spec. It may not be trivial to define what’s a “breaking” change in an automated environment, but if in doubt just decide conservatively and demand a bigger change in version as appropriate. Validating behavior would be tougher, but having some enforcement of rules and validations on the package manager side is still better than having none.