DEV Community

Cover image for How not to sort an array in JavaScript
Phil Nash
Phil Nash

Posted on • Originally published at philna.sh on

How not to sort an array in JavaScript

Array sorting is one of those things you don’t spend too long thinking about, until it stops working for you. Recently I was working with array of items in JavaScript that were not sorting at all properly and completely messing up an interface. It took me way too long to work out what went wrong so I wanted to share what happened and why it was so weird.

Basic sorting

JavaScript has a sort method available on Array objects and running it will probably do what you expect. For example:

const stringArray = ['cat', 'dog', 'ant', 'butterfly'];
stringArray.sort();
// => ['ant', 'butterfly', 'cat', 'dog']

Enter fullscreen mode Exit fullscreen mode

It’s even pretty good if you’re sorting arrays that might have members that are undefined. MDN says that “all undefined elements are sorted to the end of the array.”.

const stringArrayWithUndefined = [
  'cat',
  undefined,
  'dog',
  undefined,
  'ant',
  'butterfly',
  'zebra'
];
stringArrayWithUndefined.sort();
// => ['ant', 'butterfly', 'cat', 'dog', 'zebra', undefined, undefined]

Enter fullscreen mode Exit fullscreen mode

Gotchas

The first issue you might come across is if you find yourself with an array containing null.

const stringArrayWithUndefinedAndNull = [
  'cat',
  undefined,
  'dog',
  undefined,
  'ant',
  null,
  'butterfly',
  'zebra'
];
stringArrayWithUndefinedAndNull.sort();
// => ['ant', 'butterfly', 'cat', 'dog', null, 'zebra', undefined, undefined]

Enter fullscreen mode Exit fullscreen mode

Sorting will coerce the null to the string "null" which will appear somewhere in the middle of the alphabet.

Then there are numbers. The default JavaScript sorting algorithm is to convert all members of an array to strings and then compare their sequences of UTF-16 code unit values. This works great for arrays of strings as we’ve already seen, but it breaks down very quickly for numbers.

const numberArray = [5, 3, 7, 1];
numberArray.sort();
// => [1, 3, 5, 7]

const biggerNumberArray = [5, 3, 10, 7, 1];
biggerNumberArray.sort();
// => [1, 10, 3, 5, 7]

Enter fullscreen mode Exit fullscreen mode

In the example above, 10 gets sorted to before 3 because “10” is sorted before “3”.

We can fix this by providing JavaScript a comparison function to use to perform the sort. The function receives two items from the array and it needs to return a numeric value and whether that value is above, below or equal to zero defines how the items are sorted relative to each other. If the return value is less than zero, then the first item is sorted in front of the second, if the value is above zero then the second item is sorted in front of the first. If the return value is 0 then the items stay in the same order with respect to each other.

To sort numbers in ascending order, the comparison function is relatively simple:

const compareNumbers = (a, b) => a - b;

Enter fullscreen mode Exit fullscreen mode

Subtracting the first item from the second one satisfies the requirements above. Using this comparison function with our biggerNumberArray from earlier will sort the numbers correctly.

biggerNumberArray.sort(compareNumbers);
// => [1, 3, 5, 7, 10]

Enter fullscreen mode Exit fullscreen mode

This still works if you have undefined elements as they are ignored and sorted to the end.

const numberArrayWithUndefined = [5, undefined, 3, 10, 7, 1];
numberArrayWithUndefined.sort(compareNumbers);
// => [1, 3, 5, 7, 10, undefined]

Enter fullscreen mode Exit fullscreen mode

null causes problems again though.

const numberArrayWithUndefinedAndNull = [5, undefined, 3, null, 10, 7, 1];
numberArrayWithUndefinedAndNull.sort(compareNumbers);
// => [null, 1, 3, 5, 7, 10, undefined]

Enter fullscreen mode Exit fullscreen mode

This happens because coercing null to a number returns 0.

Number(null);
// => 0

Enter fullscreen mode Exit fullscreen mode

You could handle this in your compareNumbers function or be happy that it is consistent.

Inconsistent gotchas

The biggest problem, and this caught me out recently, comes when undefined sneaks in another way. As we’ve seen, if the array contains undefined it’s ignored and just sorted to the back. However, if you are sorting objects where the keys may be undefined this automatic sorting doesn’t happen and the results become inconsistent.

For example, if you have an array of objects where some of them have values and some don’t, trying to sort by that value will not give you the result you want.

const objectArray = [
  { value: 1 },
  { value: 10 },
  {},
  { value: 5 },
  { value: 7 },
  { value: 3 }
];
const compareObjects = (a, b) => a.value - b.value;
objectArray.sort(compareObjects);
// => [ { value: 1 },
// { value: 10 },
// {},
// { value: 3 },
// { value: 5 },
// { value: 7 } ]

Enter fullscreen mode Exit fullscreen mode

Subtracting a number from undefined or subtracting undefined from a number both return NaN and since that doesn’t lay on the scale of numbers that sort needs from the comparison function the results end up a little strange. In this case, the item that caused the problem stays where it started in the array and the other objects are locally sorted.

There are a few ways around this, but the important thing is knowing that it can happen. In my case when I came across this, I filtered out the items that didn’t have a value as they weren’t important until they did.

objectArray.filter(obj => typeof obj.value !== 'undefined').sort(compareObjects);
// => [ { value: 1 },
// { value: 3 },
// { value: 5 },
// { value: 7 },
// { value: 10 } ]

Enter fullscreen mode Exit fullscreen mode

Beware sorting

The upshot of all of this is that the sort function is not as straightforward as it might seem. Strings work, numbers need some input and while undefined is handled as a primitive you have to keep an eye on coercing nulls or undefined object values.

Have you come across problems sorting in JavaScript or other languages? I’d love to hear your stories too, so give me a shout on Twitter at @philnash.

Top comments (20)

Collapse
 
isalevine profile image
Isa Levine

Hi Phil, this is a great writeup! Do you mind if I link to this at the bottom of my recent article on JavaScript sorting? I'd love to direct people to the inconsistencies and gotchas with nulls and undefineds. :)

Collapse
 
philnash profile image
Phil Nash

I would love it if you wanted to link to it! Thanks!

Collapse
 
isalevine profile image
Isa Levine

Thanks Phil! Link has been added! :)

(dev.to/isalevine/a-quick-review-of...)

Collapse
 
pheeria profile image
Olzhas Askar

I think the biggest disadvantage of sort() is that it sorts in place.
You need to do some relatively ugly hacks like spreading:

[...array].sort().map(console.log)

in order to use the power of FP.

Collapse
 
zhuangya profile image
zhuangya

it returns an array of undefineds :/

Collapse
 
damon profile image
damon

That's because the console.log returns undefined, and it's being used in the call to .map().

var array = [9, 2, 3, 8, 1, 0]
var orderedArray = [...array].sort()

console.log(array)
// [9, 2, 3, 8, 1, 0]
console.log(orderedArray)
// [0, 1, 2, 3, 8, 9]

var logReturn = console.log("hello world")
// hello world
console.log(logReturn)
// undefined
Thread Thread
 
zhuangya profile image
zhuangya

exactly, my point is we should not use map here at all.

Collapse
 
philnash profile image
Phil Nash

Yeah, it is a shame that it acts in place as we move to code that attempts to reduce the number of side effects. At least we can work around it, the problem is just remembering to.

Collapse
 
macorifice profile image
Stefano

Hi Phil,
would not have been the case to use lodash for undefined and null ?

Collapse
 
vjarysta profile image
Valentin Jarysta • Edited

This would have worked as well, therefore you might not need lodash.

Collapse
 
philnash profile image
Phil Nash

Hi Stefano, not sure what you mean there. Could you explain more?

Collapse
 
macorifice profile image
Stefano
Thread Thread
 
pavelloz profile image
Paweł Kowalski

Wow. Using lodash for that. Wow. :)

Thread Thread
 
philnash profile image
Phil Nash

Oh, sure, I could have used that. I’d probably prefer to just use an array filter though, as I do later in the post, as it will only filter the things I want and don’t incur the overhead of requiring a library like (or even just a function from) lodash.

Collapse
 
pavelloz profile image
Paweł Kowalski

Hmm, i always filter the falsy values by doing arr.filter(x => x) (i probably would do that if i wanted <1 numbers in there though ;)

Collapse
 
pheeria profile image
Olzhas Askar

I like

array.filter(Boolean)
Collapse
 
pavelloz profile image
Paweł Kowalski

Nice one, didnt know that one :)

Thread Thread
 
philnash profile image
Phil Nash

They are both cool filter expressions! 😎

But... they both lose 0 or '' which you might not want. That's why I like the undefined check.

> const array = [-1, 0, 1]
[ -1, 0, 1 ]
> array.filter(Boolean)
[ -1, 1 ]
> array.filter(x => x)
[ -1, 1 ]
Collapse
 
acostalima profile image
André Costa Lima

Very informative, thank you! 😊👌💪

Collapse
 
philnash profile image
Phil Nash

Glad you enjoyed it, thanks!