JavaScript Array.sort() Pitfalls (and How to Fix Them)
Array.sort() converts elements to strings by default, so numbers sort out of order. Here's why, and how a compare function fixes it.
Array.prototype.sort() sorts elements as strings by default, not as numbers — which means [10, 2, 1].sort() produces [1, 10, 2] instead of the numeric order you’d expect. This single default trips up more JavaScript code than almost any other built-in method, and it’s worth understanding exactly why it happens and how to reliably avoid it.
Why numbers sort like strings
Without a compare function, sort() converts every element to a string with String() and compares the results using UTF-16 code unit order. That’s lexicographic (dictionary-style) comparison, not numeric comparison. String comparison looks at the first character first, so "10" comes before "2" because "1" is less than "2" — the rest of the string never matters once the first character differs.
[10, 2, 1].sort();
// [1, 10, 2] — not [1, 2, 10]
["banana", "Apple", "cherry"].sort();
// ["Apple", "banana", "cherry"] — uppercase sorts before lowercase
The string case is just as surprising: capital letters have lower code points than lowercase letters in UTF-16, so "Apple" sorts before "banana" regardless of alphabetical intent.
The fix: a compare function
Passing a compare function to sort() overrides the default string conversion entirely. The function takes two elements, a and b, and returns a number:
- Negative →
acomes beforeb - Positive →
acomes afterb - Zero → treat them as equal (relative order preserved)
For numbers, the idiomatic compare function is a subtraction:
[10, 2, 1].sort((a, b) => a - b);
// [1, 2, 10]
[10, 2, 1].sort((a, b) => b - a);
// [10, 2, 1] — descending
For case-insensitive string sorting, or sorting that needs to respect locale-specific rules (accents, non-Latin scripts), use localeCompare instead of relying on raw code point order:
["banana", "Apple", "cherry"].sort((a, b) => a.localeCompare(b));
// ["Apple", "banana", "cherry"] in locale-aware alphabetical order
Sorting objects by a property
The same pattern extends to sorting arrays of objects — just compare the relevant property inside the function instead of the raw elements:
const users = [{ age: 34 }, { age: 19 }, { age: 45 }];
users.sort((a, b) => a.age - b.age);
// sorted ascending by age
For multi-key sorting (say, sort by last name, then by first name for ties), chain comparisons and return the first non-zero result:
people.sort((a, b) => a.lastName.localeCompare(b.lastName) || a.firstName.localeCompare(b.firstName));
sort() mutates the original array
Unlike map, filter, and reduce, sort() doesn’t return a new array — it sorts in place and returns a reference to the same array. If you need to preserve the original order (say, the array came from props or shared state), copy it first:
const sorted = [...original].sort((a, b) => a - b);
Forgetting this is a common source of bugs when an array is shared across multiple parts of an application — sorting it in one place silently reorders it everywhere else it’s referenced. Array destructuring combined with the spread operator, as above, is the cleanest way to make a shallow copy inline.
Is Array.sort() stable?
Yes — as of the ECMAScript 2019 specification, Array.prototype.sort() is required to be a stable sort: elements that compare equal keep their relative order from before the sort. This matters for multi-pass sorting, where you might sort by one key, then stably sort by a second key, and expect ties from the first pass to remain in their prior order. Before that guarantee was standardized, engines were technically free to use unstable algorithms, so older code that depends on stability without pinning a modern runtime should be treated with a bit more caution — see quicksort vs mergesort for why some classic sorting algorithms are stable and others aren’t by nature.
Quick reference
| Goal | Compare function |
|---|---|
| Ascending numbers | (a, b) => a - b |
| Descending numbers | (a, b) => b - a |
| Case-insensitive strings | (a, b) => a.localeCompare(b) |
| Sort by object property | (a, b) => a.prop - b.prop |
| Multi-key sort | Chain with ` |
The takeaway
sort() without a compare function treats everything as a string, which is almost never what you want for numbers and can be surprising even for strings with mixed casing. Always pass an explicit compare function — a subtraction for numbers, localeCompare for locale-aware strings — and remember that sort() mutates in place, so copy the array first if the original order needs to survive elsewhere in your program.
Keep reading
Takina · · 4 min read Web Streams API Explained: Readable, Writable, Transform
The Streams API lets JavaScript process data as chunks arrive instead of buffering it all in memory. How ReadableStream, WritableStream, and pipes work.
Takina · · 4 min read npm and pnpm Workspaces: Managing a Monorepo
Workspaces let npm and pnpm manage multiple packages in one repository, sharing a single dependency tree and letting packages reference each other locally.
Takina · · 5 min read toSorted, toReversed, with(): JS's New Array Methods
toSorted(), toReversed(), toSpliced(), and with() copy an array instead of mutating it, fixing a long-standing footgun in JavaScript's Array API.