Array.prototype.sort mutates the array in place and, without a comparator, converts every element to a string and sorts lexicographically. Supplying a comparator makes the ordering explicit, but the comparator has to satisfy a simple contract for the result to be well defined.
Before you start
You should be comfortable with arrays, Array.prototype.sort, and returning numbers from functions. This article is about the comparator contract and stability; it is not an algorithm analysis of the sort implementation.
Step-by-step walkthrough
Step 1: Always pass a comparator for numbers
The default converts elements with String, so [10, 9, 80].sort() yields [10, 80, 9] because "10" < "80" < "9". A numeric comparator (a, b) => a - b returns a negative number when a sorts first, positive when b first, and zero for equal order.
Step 2: Keep the comparator pure and total
The comparator should depend only on its two arguments and return a consistent sign for the same pair; it must not mutate the array or rely on external state. a - b on strings evaluates to NaN, which the engine treats as zero and leaves the order unpredictably unchanged. Use a.localeCompare(b) or explicit comparisons for text.
Step 3: Lean on stability for tie-breaking
Modern engines implement a stable sort, so elements that compare equal keep their relative order. That makes a single sort key safe: sort once by the primary value and rely on the pre-existing order for ties, or write a composite comparator that breaks ties explicitly when the input order is not guaranteed.
Worked scenario
Run this with Node.js. The two elements with score 2 keep their original relative order.
const rows = [
{ name: 'b', score: 2 },
{ name: 'a', score: 2 },
{ name: 'c', score: 1 }
];
rows.sort((a, b) => a.score - b.score);
console.log(rows.map((row) => row.name)); // ['c', 'b', 'a']Walk through the example
The comparator compares only score, so c moves ahead of the two score-2 rows, and because the sort is stable, b stays before a. The array is sorted in place: rows itself is reordered, not a copy. If the original order were undefined, the result for the tied rows would be unspecified, which is why an explicit tie-break matters for deterministic output.
Common mistake
Calling .sort() on numbers and expecting numeric order, or using a - b on strings and trusting the result. Another frequent slip is assuming sort returns a new array: it returns the same, mutated array, so code that still references the original sees the reordered values.
Verify the behavior
Assert the numeric order for a mixed array such as [10, 9, 80]. Assert that equal keys preserve input order for a stable-expected case. Test empty and single-element arrays, and confirm that a comparator that returns NaN (from string subtraction) does not accidentally reorder numeric-looking strings. If you need an immutable result, copy first with [...items].sort(...).
Interview exercise
Sort records by score descending, and by name ascending when scores are equal.
Answer and reasoning
Return b.score - a.score for the primary key. When that is zero, fall back to a.name.localeCompare(b.name) for the secondary key. Writing the tie-break in the comparator makes the result deterministic regardless of the input order, instead of relying on stability that only holds for equal keys and depends on how the array was built.
Follow-up discussion
Is the sort stable everywhere? Since ES2019 it is required to be stable, but older engines may not be, so do not rely on stability for correctness-critical ordering. Does a bad comparator crash? No: an inconsistent comparator produces an implementation-defined order rather than an error, which is why a subtly wrong sign is easy to miss in review.
Continue learning
Compare value-comparison subtleties in JavaScript equality and coercion and read the MDN sort reference. Try the JavaScript interview questions.