Semver has failed us June 19, 2014

As a maintainer of many popular packages such as express, I, along with the other maintainers, have come to realized that semantic versioning simply does not work in practice. It has caused a lot of bikeshedding, creating non-constructive discussions in the repository. It generally makes development actually more difficult.

Recently, some people have created some ideas about how to improve versioning. I first proposed getting rid of versions < 1 all together as 0.x.x modules are inherently unstable due to its lack of specification. Damon Oehlman proposed slimver, a stricter variant of semantic versioning.

I'm proposing ferver, which changes the semantics of semver to be more practical with breaking changes. You can read more about it on the GitHub page, but first, let's talk about the failures of semver.

Patches break

Part of the major issues with semver is that patchs could break. A feature could be "fixed", but a consumer could have relied on that very buggy feature, and patching it would break their app. An example are times when a library simply does not behave according to specifications and fixes it to cohere to the specification.

An example is with Express 3.4.3. A bug with redirects was fixed, but some users were relying on that bug. Thus, even though it was a patch (3.4.2 -> 3.4.3), it broke some people's apps. A user asked to at least bump a minor version, but if we strictly cohere to semver, we can't because it's a patch, not a new feature.

I greatly sympathize with this user, and this particular case is essentially the first time I realized, "semver doesn't work".

0.x.x is anarchy

Versions < 1.0.0 do not have semantic versions according to semver. These are considered libraries "in development" and developers could version however they see fit. Thus, developers bump the minor or patch numbers however they like. It's anarchy.

The problem isn't that versions less than < 1.0.0 are allowed. Nope, it makes sense for libraries to be able to break changes before declaring a stable 1.0.0. The problem is that there are no semantics to 0.y.z versions. Consumers simply don't know how to depend on these modules using version ranges without introducing a significant amount of risk into their app.

Pinned dependencies

The current solution to the above problems is to pin all dependencies. However, this is absolutely stupid and annoying to me. If you're pinning versions, you're reducing the package manager to a glorified CURL.

It makes maintaining very difficult and annoying. Duplicate dependencies are bad, especially in frontend development where file size matters, which is one reason frontend developers prefer Bower's flatter dependency directory vs. npm's nested. When every library pins, you're going to have a lot of duplicate dependencies, even if they're the same version! Not everyone has the time to update every patch and make a new release.

Even if you control the dependencies, some people like pinning dependencies. To me, this is absolutely silly, but it is necessary because patchs can break. For example, if you look at the 2.x branch of Connect, you'll see that all the commits are just dependency updates. However, they all do not break backwards compatibility because Connect coheres to semver. These updates should not be necessary and should be available just from typing npm update.

Slower development

If you look at Express' current issues, you'll see that half of them are planned for the next major version, 5.0.0:

Express Issues

The problem with this is that these minor issues may be backwards incompatible, but they are nevertheless issues that consumers would have to deal with until v5.0.0.

For example, Update path regexp functionality would break routes for a very few people who write really weird routes, but it introduces many new features and provides better semantics. This introduces a lot of benefits for most developers while introducing risk to a very few developers.

Ideally, these changes should happen as fast as possible, but in a way that tells consumers, "Hey, this is new and improved, but it might break your app. Proceed with caution.". There's no way to say that with semver except with major version releases.

Prereleases

Semver does not have a good scheme for prereleases. People append all sorts of weird strings to their versions. 1.0.0-beta1. 1.0.0-3.2.3.1. Who knows what these mean. It only makes libraries more difficult to consume as well as confuse consumers.

Since semver is liberal with these affixes, package managers like npm have trouble dealing with them. For example, if you use 1.5.0-beta1 of a library and the latest version is 1.4.0, npm outdated will mark 1.5.0-beta1 as outdated. Yeah, I don't think that's outdated.

A good versioning system would allow for prereleases and beta builds while still cohering to x.y.z versioning. It would also be able to allow consumers to distinguish between prereleases and releases semantically.

The fear of x.0.0

Many developers never release v1.0.0 of their projects. With semver, this is really annoying because you have to pin to reduce the risk of breaking changes.

But others absolutely hate when libraries update the major version. They see it as a sign of "instability", but according to semver, these backwards-incompatible changes could be something as insignificant as returning null instead of undefined, which wouldn't break most people's apps.

The problem here is that people don't associate major versions with "breaking changes". They associate it with the character, purpose, and philosophy of a library. 0.x.x means "We don't know what we're doing". 1.0.0 means "We think we know the direction of this library.". 2.0.0 means "We're changing directions a little bit". 3.0.0 means "We're changing directions a little bit, again".

Semver simply has the wrong semantics. Not every breaking change is a fundamental difference in a library's character. People are okay if you break things here and there, but it must be easy for them to know when you break something. This can only be done with a major version bump with semver.

Solving semver

There are two ways to solve semver: bump major versions often, or use a different versioning scheme.

Currently, I release most new modules I write as 1.0.0 and liberally bump major versions. For example, koa-session is already at 2.0.0, but koa hasn't even reached 1.0.0. Hell, co has already reached 3.0.6 and ES6 isn't even finalized.

This is why I proposed having semver drop versions < 1. Who cares if you're at version 36 like Chrome. I just want to know if something would break! But this is not suitable for most people since, due to semver's semantics, they correlate a lot of major version bumps with the library being "unstable".

The other solution is just use a different versioning scheme. This is what ferver - versioning based on whether a change is breaking. Please, don't use it though. It's only a thought.