Skip to main content

Sorting

Sorting in JavaScript

Written by Updated

You will almost never write a sorting algorithm at work. You will constantly write comparators - and getting one wrong is a bug that survives review because the output looks nearly right.

The default is wrong for numbers

The bug everyone hits once

javascript

const nums = [10, 9, 100, 1]

nums.sort()
console.log(nums)   // [1, 10, 100, 9]  — sorted as strings

nums.sort((a, b) => a - b)
console.log(nums)   // [1, 9, 10, 100]  — correct

Without a comparator, sort converts every element to a string. "100" comes before "9" the same way "apple" comes before "banana".

How a comparator works

Return a negative number to put a first, positive for b first, zero to leave them alone.

Comparators you will actually write

javascript

const items = [10, 2, 33, 4]
const names = ["Zoe", "ada", "Bob"]
const users = [
  { name: "Ada", age: 36 },
  { name: "Bob", age: 36 },
  { name: "Cy", age: 21 },
]
const posts = [{ published: "2024-01-02" }, { published: "2023-05-01" }]

// Numbers, ascending and descending.
items.sort((a, b) => a - b)
items.sort((a, b) => b - a)

// Strings, respecting accents and locale.
names.sort((a, b) => a.localeCompare(b))

// By a field.
users.sort((a, b) => a.age - b.age)

// By one field, then another as a tiebreak.
users.sort((a, b) => a.age - b.age || a.name.localeCompare(b.name))

// Dates.
posts.sort((a, b) => new Date(b.published) - new Date(a.published))

console.log(items)
console.log(names)
console.log(users.map((u) => u.age))

The || tiebreak works because a comparator returning 0 is falsy, so the second comparison runs only when the first ties.

sort mutates

sort changes the array in place and returns the same array. That surprises people who expect map-like behaviour.

Sorting without mutating

javascript

const original = [3, 1, 2]

const sorted = [...original].sort((a, b) => a - b)
// or, in newer runtimes:
const sorted2 = original.toSorted((a, b) => a - b)

console.log(original)  // [3, 1, 2] — untouched

Stability, and why it matters

A stable sort keeps equal elements in their original relative order. JavaScript's sort has been required to be stable since ES2019, and you can rely on it.

That is what makes sorting by two things possible in two passes: sort by the secondary key first, then by the primary. The stable sort preserves the first ordering within ties.

What it costs

Array.prototype.sort is O(n log n). V8 uses TimSort, which is close to O(n) on data that is already partly ordered - common in practice, and a reason not to hand-roll your own.

The comparator runs O(n log n) times, so keep it cheap. Computing a value inside a comparator is a classic slow path:

Precompute instead of recomputing

javascript

const posts = [
  { date: "2024-03-01" },
  { date: "2023-01-15" },
  { date: "2024-01-02" },
]

// Slow — parses dates on every comparison.
posts.sort((a, b) => new Date(a.date) - new Date(b.date))

// Fast — parse once, then sort by the number.
const withTime = posts.map((p) => ({ post: p, time: new Date(p.date).getTime() }))
withTime.sort((a, b) => a.time - b.time)
const sorted = withTime.map((x) => x.post)

console.log(sorted.map((p) => p.title))