Articles

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.

Takina Takina · · 3 min read
Dark-themed code editor showing JavaScript

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 → a comes before b
  • Positive → a comes after b
  • 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

GoalCompare 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 sortChain 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.

Takina 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.

#JavaScript #Web Development #Developer Tools
Takina 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.

#JavaScript #Web Development #Developer Tools