Best Practices for Javascript Library Versioning
Please, please start using semver right
I spend most of my time on this blog talking about Clojure, but IRL I work on a lot of projects written in a lot of languages. Lately, much of my time has been spent writing and maintaining React-based frontends in Javascript.
As far as I can tell, no language has solved the problem of dependency management. Some libraries are better than others, but upgrading dependencies is always an uncomfortable experience. That being said, I’ve never experienced more pain doing so than in Javascript’s ecosystem.
If you follow me on Twitter, you already have some idea how I feel about Javascript’s dependency situation. There are lots of reasons for it. One of the reasons is just that Javascript has a very active ecosystem, with tons of libraries changing all the time. But in my experience, the primary reasons is that library developers often just do versioning wrong.
(If you have an hour to watch a video, Rich Hickey’s Spec-ulation talk is a must-watch for any software developer, and basically covers everything I’m about to say but better.)
Why does versioning exist?
Versioning exists because, fundamentally, we want to make our software better over time. Nobody’s perfect, and sometimes bugs or regrettable API decisions make it out in a release, and we want to be able to fix those problems. But from the point of view of your library consumers, changes are bad. Changes represent risk and effort. So you, the library author, should endeavour to keep the API that you expose consistent.
The problem of balancing fixes and improvements with is an old one, and many solutions exist, but the one that’s been chosen by the Javascript ecosystem to solve this problem is Semantic Versioning (semver).
The Golden Rule
Semver has a lot of rules, but the core is very simple:
- Versions take the format MAJOR.MINOR.PATCH
- PATCH versions are for bug fixes that do not change your public API
- MINOR versions are for changes that add functionality, without otherwise changing the your public API
- MAJOR versions are for changes that change your public API
Seems easy enough, but note that the above prohibits breaking changes on any except major versions. But what is a breaking change really?
What is a breaking change?
There are two major schools of thought here.
The first school is that a breaking change is any change that doesn’t alter the intended functioning of the library.
The second school – the only correct one, in my opinion – is that a breaking change is any change in behavior, even if the old behavior was unintentional or the new implementation is way better, honest! I would like to advance this point of view in the Javascript community.
I think this is made all the more important by the way Javascript’s tooling behaves. npm install --save
and
yarn add
both add dependencies using the ^x.y.z
syntax by default, which generally results in minor and patch versions
being automatically upgraded. If libraries are constantly breaking on minor or patch releases, this quickly ends up
being very unpleasant.
What to do as a library author?
There’s no one solution to this problem, but here are some suggestions from me, a library maintainer and potentially your user.
1. Embrace more major versions
One of the reasons that people avoid major releases is that it seems to imply major revisions. But this may lead to minor releases with breaking changes, which as we know leads to madness. Instead, consider releasing major versions more frequently.
Lots of software releases major versions routinely. What version is your browser at right now? Or, consider React 15 and 16 (leaving behind their up-to-0.14 beta period), which have been broadly non-breaking. This is the right way to do it.
Another way to look at major releases: If you want to overhaul your library’s public API, you may as well give it a different name, because it’s in effect a different library ( as an example, remember Angular and Angular 2?). Don’t be Angular; use the Major version to advertise improvements to your API.
2. Add instead of changing
There are good reasons not to make a new major release for every bugfix, but luckily there are other ways not to break your library too.
Say you find a function that really isn’t working the way you intended. How do you resolve this oversight without creating a new major release? Simple! You a add the intended functionality under a new name and do a minor release. Add a note in your docs to use the new function instead so that new users know about the change, and everyone is happy!
3. Use suffix versioning for pre-release software
Do me a favor and skip the 0.y.z version. If you’re publishing “pre-release” versions of your software, and you
expect it to break all the time, prefer “1.0.0-beta17” to “0.1.17”, for the sake of all of us who would rather
not police our ^
carats. Or, even better, call your package something like “mylibrary-beta” and then proceed with
regular semver until it’s time to release a stable version!
I guess my point is, please start using semver properly and completely.
4. Be strict about your own dependencies
If you’re not writing stuff like left-pad
, your library probably has
dependencies of its own. If those dependencies are using ^
versioning,
they’re liable to break at random, because as everyone who’s been working with
Javascript for more than 6 months knows, minor versions cannot be trusted.
Tips for library users
Unless this post has a lot more impact than I expect, chances are you’re still going to get burned by questionable version hygiene. Here are a few tips to help with that.
- Don’t rely on minor versions to be non-breaking: Minor versions are not safe in Javascript today, so use the
~1.1
or the1.1.x
syntax in yourpackage.json
file. - Use yarn: Yarn will automatically lock down version numbers until you explicitly upgrade those dependencies, which should help you avoid issues until you explicity upgrade.
Other advice is welcome in the comments if you have it!