~/blog

Array.sort with a boolean comparator is a silent no-op

published

#javascript#arrays#sorting

TL;DR

arr.sort((a, b) => a > b) is not a comparator. It returns a boolean, which coerces to 1 or 0 — never a negative number — so it violates what the spec requires and the sort order becomes implementation-defined. On V8 (Node, Chrome) the result is worse than wrong: it does nothing at all. Use (a, b) => a - b.

The problem

This reads like it should work, and it is a common thing to write from memory:

const scores = [5, 4, 3, 2, 1];
scores.sort((a, b) => a > b);
console.log(scores); // [5, 4, 3, 2, 1]

The array comes back exactly as it went in. No error, no warning, no NaN — just unsorted data flowing into whatever reads it next.

It is not limited to reversed input. Across 2,000 random arrays of 1–80 integers on Node v26.2.0, the output was byte-identical to the input every single time. On V8, a boolean comparator is a complete no-op:

const bad = (a, b) => a > b;
const eq = (x, y) => JSON.stringify(x) === JSON.stringify(y);

let unchanged = 0;
for (let t = 0; t < 2000; t++) {
  const a = Array.from(
    { length: 1 + Math.floor(Math.random() * 80) },
    () => Math.floor(Math.random() * 1000),
  );
  if (eq([...a].sort(bad), a)) unchanged++;
}
console.log(unchanged); // 2000

Why nobody catches it

Because the one input that makes it look correct is the input people test with:

[1, 2, 3, 4, 5].sort((a, b) => a > b); // [1, 2, 3, 4, 5] — "passes"
[1, 2, 4, 3, 5].sort((a, b) => a > b); // [1, 2, 4, 3, 5] — one swap, missed

A fixture that is already in order sails through. The bug ships, and it surfaces later as “the leaderboard is in insertion order” or “the dropdown isn’t alphabetical.”

Why it happens

Array.prototype.sort expects the comparator to return a number, and it reads the sign of that number: negative means a comes first, positive means b comes first, zero means treat them as equal.

Since ES2015, the return value is passed through ToNumber. A boolean converts cleanly, which is precisely the trap — there is no error to raise:

ExpressionReturnsToNumberMeaning to sort
a > b when a is greatertrue1b comes first
a > b when a is smallerfalse0treat as equal
a - b when a is smaller-3-3a comes first

The comparator can only ever say “swap” or “these are equal”. It has no way to say “a comes first”, so nothing is ever moved left, and a merge-based sort has nothing to act on.

The spec does not promise you any particular wrong answer here. ECMA-262 requires comparefn to be a consistent comparison function — part of that definition is that the returned value is a Number and is not NaN. A boolean-returning function cannot satisfy it, and the spec’s stated consequence is blunt:

If comparefn is present and not undefined but is not a consistent comparison function for the elements of the array, the behavior of sort is implementation-defined.

So “it returns the array unchanged” is a V8 observation, not a guarantee. A different engine, or a different V8 version, may produce a different wrong order. V8 itself swapped Array#sort from Quicksort to TimSort in V8 v7.0 / Chrome 70, which is exactly the kind of change that alters what an invalid comparator does.

The sibling bug: no comparator at all

Related, and worth knowing if you are already auditing sorts. With no comparator, elements are compared as strings:

[10, 9, 1].sort(); // [1, 10, 9]

That one at least looks wrong immediately.

What to do

For numbers, subtract:

scores.sort((a, b) => a - b); // ascending
scores.sort((a, b) => b - a); // descending

For strings, use localeCompare — it already returns a negative/zero/positive number:

names.sort((a, b) => a.localeCompare(b));

For booleans as a sort key, convert them to numbers rather than comparing them directly:

tasks.sort((a, b) => Number(b.done) - Number(a.done)); // done first

For a comparison you can only express with < and >, return the three values explicitly:

items.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));

TypeScript catches this one

Worth knowing, because it is a rare case where the type system earns its keep immediately. sort is typed as compareFn?: (a: T, b: T) => number, so a boolean-returning arrow is a compile error:

error TS2345: Argument of type '(a: number, b: number) => boolean' is not
assignable to parameter of type '(a: number, b: number) => number'.
  Type 'boolean' is not assignable to type 'number'.

If the code is plain JavaScript, a // @ts-check comment at the top of the file gets you the same diagnostic from tsc without converting the file to TypeScript.

Caveats

References