Number sorting gotcha with JavaScript

Problem: Sorted arrays are coming back in an unexpected order... sometimes. Safari is behaving differently than Chrome and Node.js. Let’s look at what to do.

There’s now a small, solid group working on Astoria Digital’s MuckRock dataviz project. In fact, Chris DeLuca recently published a post about some developer onboarding issues with the dataviz library we are using for the project. We have prevailed, but it took some putting our heads together last weekend.

While we were troubleshooting our new visualization, we ran into another issue JavaScript developers frequently encounter: return values coming back in an unexpected order... sometimes. In this case, the issue was browser-specific, where a list’s order was fine in Safari, but seemingly random in Chrome.

A simple version of the problem

Say you have a list of prices that you want to sort:

const prices = [3, 5, 370, 8];
const sortedPrices = prices.sort();

What do you think sortedPrices will look like?

For Chrome and Node.js, you are correct if you guessed:

[ 3, 370, 5, 8 ]

Because (via MDN):

The default sort order is ascending, built upon converting the elements into strings, then comparing their sequences of UTF-16 code units values.

So the output above is the correct ascending order when the numbers are coerced to strings:

console.log("3" < "350"); // true

console.log("350" < "5"); // true; gotcha!

The wild thing here is that Safari does what you would expect, leading to more confusion:

[ 3, 5, 8, 370 ]

So using Array.prototype.sort() with no argument is not going to work for reliably sorting numbers. To make sure the sort behaves as we expect everywhere, we’ll have to supply our own compare function.

This won’t work

We could try using a greater-than/less-than comparitive operator:

const sortedPrices = prices.sort((second, first) => first < second);

But recall that the parameters you receive in the callback (second and first) are strings, not numbers, so here’s what you’ll get:

[ 3, 5, 370, 8 ]

Because:

“3” < “5”      // true
“5” < “370”    // true
“370” < “8”    // true!

The comparison operators don’t coerce strings to numbers, so you just get a different flavor of the problem we saw when passing no argument to Array.prototype.sort().

But this will work

JavaScript will coerce the string parameters to numbers if we perform a mathematic operation on them. So instead of comparing, we can simply subtract them:

const prices = [3, 5, 370, 8];
const sortedPrices = prices.sort((second, first) => second - first);

If second - first is a negative number, second will be sorted down in the array, step-by-step, until it ends up in its correct spot in the ascending order.

Our new compare function gives us:

[ 3, 5, 8, 370 ]

Problem sorted.