JavaScriptSorting Arrays

Sorting Arrays

Sorting looks simple — call arr.sort() and you are done. In practice the default behaviour surprises almost every newcomer, the method mutates the array, and modern JavaScript has added a non-mutating alternative. This page covers the four things you need to know: the quirky default, how to write a compare function, what stable means, and the new toSorted / toReversed methods.

The default sort is alphabetical — even for numbers

Without a compare function, sort converts every element to a string and orders the strings by UTF-16 code units. Numbers, dates, objects — all stringified first. That is almost never what you want for numbers.

JS
[10, 1, 21, 2, 100].sort();
// [1, 10, 100, 2, 21]   — string order!

["banana", "Apple", "cherry"].sort();
// ["Apple", "banana", "cherry"]   — uppercase comes first in UTF-16
Always pass a compare function for numbers
The number example above is the single most-asked JavaScript interview question for a reason. Forget the compare function and your "sorted" array will look right for one-digit numbers and break the moment you add a 10.
Writing a compare function

A compare function takes two elements a and b and returns:

  • Negative number — a should come before b.

  • Positive number — a should come after b.

  • Zero — keep their relative order (the sort is stable, see below).

JS
const nums = [10, 1, 21, 2, 100];

nums.sort((a, b) => a - b);   // ascending:  [1, 2, 10, 21, 100]
nums.sort((a, b) => b - a);   // descending: [100, 21, 10, 2, 1]

// Strings, case-insensitive
const words = ["Banana", "apple", "Cherry"];
words.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
// ["apple", "Banana", "Cherry"]

// By a key
const users = [
  { name: "Lin",   age: 31 },
  { name: "Ada",   age: 25 },
  { name: "Pedro", age: 28 },
];
users.sort((a, b) => a.age - b.age);
// Ada(25) → Pedro(28) → Lin(31)
Multi-level sort with the || trick
Chain compare results with `||`: `(a.country.localeCompare(b.country)) || (a.age - b.age)`. The first non-zero result wins, so it sorts by country and breaks ties by age.
sort() mutates — and returns the same array

Like reverse and splice, sort changes the array in place and also returns it (a convenience that often hides the mutation).

JS
const xs = [3, 1, 2];
const ys = xs.sort();   // ys is the SAME array as xs
ys[0] = 99;
xs;                     // [99, 2, 3]   — surprise!

// To sort without mutating: copy first
const zs = [...xs].sort();
// or use toSorted (next section)
Stable sort

A sort is stable if elements that compare equal keep their relative order. Modern JavaScript guarantees stability (since ES2019). That matters when you sort by a partial key.

JS
const records = [
  { name: "Ada",   country: "UK" },
  { name: "Tina",  country: "UK" },
  { name: "Lin",   country: "TW" },
  { name: "Pedro", country: "BR" },
];

records.sort((a, b) => a.country.localeCompare(b.country));
// Stable: within "UK", Ada still appears before Tina
// because that was their original order.
Why stability matters
Stable sorting lets you build multi-criteria orderings by sorting from least-important to most-important key. Sort by name first, then by country: within each country, names stay in alphabetical order.
toSorted, toReversed, toSpliced, with

ES2023 added four non-mutating siblings of the classic mutators. They return a new array and leave the original alone — exactly what frameworks like React want.

JS
const xs = [3, 1, 2];

const sorted   = xs.toSorted((a, b) => a - b);   // [1, 2, 3]; xs unchanged
const reversed = xs.toReversed();                // [2, 1, 3]; xs unchanged
const replaced = xs.with(0, 99);                 // [99, 1, 2]; xs unchanged
const trimmed  = xs.toSpliced(1, 1);             // [3, 2];    xs unchanged

xs;   // still [3, 1, 2]

These methods are available in all modern browsers and Node 20+. If you target older environments, fall back to spread-then-mutate: [...xs].sort(cmp).

Locale-aware sorting

Plain string comparison breaks for non-English text. Intl.Collator or String.prototype.localeCompare handles accents, case rules, and digits-in-text the way humans expect.

JS
const items = ["item 10", "item 2", "item 1", "ítem 3"];

// Naive
[...items].sort();
// ["item 1", "item 10", "item 2", "ítem 3"]   — wrong

// Locale-aware with numeric handling
const collator = new Intl.Collator(undefined, {
  numeric: true,
  sensitivity: "base",
});
[...items].sort(collator.compare);
// ["item 1", "item 2", "ítem 3", "item 10"]
Sorting cheat sheet
  • Default sort() is string order — pass a compare function for everything else.

  • Ascending numbers: (a, b) => a - b. Descending: (a, b) => b - a.

  • For strings, prefer a.localeCompare(b) to < / >.

  • Sort is stable in modern engines — chain compares with || or sort in passes.

  • sort mutates; use toSorted (or [...arr].sort()) when you need a copy.