Any change can be a breaking change

I like writing small JavaScript libraries, and I think it’s super important to use semver correctly so anyone can get a recent version of my libraries knowing that it won’t break if they keep using the same major version. In other words: if you don’t change your major version all changes must be backwards compatible.

But in a dynamic language like JavaScript it might not be obvious when you are breaking something. I’ll give three examples:

Additional arguments

Imagine you have implemented a function like this and somebody is using it:

function queryByCategory (category) {
return allItems.filter((item) => item.category === category)
}
// somebody uses your library
var items = ['bar', 'restaurant', 'pub'].map(queryByCategory)

It works well and at some point somebody decides to add an optional argument `limit` to the function:

function queryByCategory (category, limit=allItems.length) {
return allItems.filter((item) => item.category === category)
.slice(0, limit)
}

So now you can limit the number of results returned. You ship the new version and suddenly that guy that was using your library gets weird results. For example he’s not getting any values for the “bar” category. WTF!

This is because the `Array.map()` function passes to the callback the index of each element as second argument. And now `queryByCategory()` is using that parameter to limit the number of results. This was not expected by the user of your library of course. It was working fine and now you introduced a breaking change!

This is why I prefer to be explicit like this:

var items = ['bar', 'restaurant', 'pub']
.map((category) => queryByCategory(category))

Instead of passing `queryByCategory` directly to `.map()` I’m creating an arrow function. This would have prevented the problem and many times makes things clearer.

However, this doesn’t stop you from worrying about the breaking changes you may have been introducing in your libraries by just adding optional parameters to existing functions :-/.

Returning something where you didn’t

Another example is when you decide to start returning a value in a function that was never returning anything. It makes no difference weather it is a sync function, a callback or a promise. Let’s see an example:

db.findSomething()
.then((result) => {
if (result) return db.deleteSomething(result)
return true
})
.then((missing) => {
if (missing) {
console.log('the result was missing')
} else {
console.log('the result was NOT missing')
}
})

You have a few promises. First you query something in the database. If it exists you delete it, but in the next promise you want to remember if it was found or not. This works and at some point you upgrade the database library. The new version of the library returns the number of affected rows in the `deleteSomething` method so, when it deletes the object it passes “1” to the next promise and that’s a truly value. Your code stops working as you expected.

Returning additional information

Another interesting breaking scenario is when you return additional fields in an object. What could go wrong if old code won’t use the new fields? Well, there could be assumptions that your changes may have changed. Imagine there is a library that returns the EXIF of an image, as pairs of key-value strings.

lib.exif('something.jpg')
.then((attr) => {
var values = Object.keys(attr).map((key) => attr[key])
var arr = values
.filter((val) => val.indexOf('_something_') === 0)
})

You are interested in the values that contain “_something_”. That’s it. Now imagine that the library returns in a new version an additional key-value pair like `bytes_size=total_size_of_exif_info` and it’s an integer. Boom! Your code will break because an integer doesn’t have `.indexOf()`.

What to do

Both, developers that use third-party code and developers that develop libraries should be careful with this. I recommend:

  • Being more explicit when using third-party code, and use defensive programming.
  • Have good test coverage for detecting breaking changes quickly.
  • Be careful when changing the input and output of existing functions, or just create new functions or make the new functionality to be available only if you opt-in with flags.
Like what you read? Give Alberto Gimeno a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.