Back to JavaScript
JavaScript
easy
mid

What algorithm does Array.prototype.sort use, and what is the output of sorting an array with null and undefined?

Modern engines use **TimSort** (V8 since ~2018) — stable, O(n log n). Default comparator is **string** (lexicographic): `[1, null, 5, 2, undefined].sort()` → `[1, 2, 5, null, undefined]` because `undefined` is always moved to the end and `null` stringifies to `"null"`. Always pass a comparator for numbers.

3 min read·~6 min to think through

What sort uses now

V8, JavaScriptCore (Safari), and SpiderMonkey (Firefox) all use TimSort for Array.prototype.sort. It's:

  • Stable — equal elements keep relative order (mandated by ECMAScript 2019).
  • O(n log n) worst-case, O(n) best-case on partially sorted data.
  • A hybrid merge + insertion sort.

Pre-2018 V8 used QuickSort which is unstable; modern code can rely on stability.

The default comparator

When no comparator is passed:

  1. Skip undefined elements; move them to the end.
  2. Convert remaining to strings.
  3. Compare strings lexicographically (UTF-16 code units).

So:

js
[1, null, 5, 2, undefined].sort();
// String sort: "1" < "2" < "5" < "null"; undefined at end
// → [1, 2, 5, null, undefined]
js
[10, 1, 2].sort();
// → [1, 10, 2]  — "1" < "10" < "2" lexicographically

This is the classic interview trick. Always pass a comparator for numbers:

js
[10, 1, 2].sort((a, b) => a - b);  // [1, 2, 10]

Comparator rules

(a, b) => negative → a before b. (a, b) => 0 → equal (preserves order; stable). (a, b) => positive → a after b.

Never return booleans or rely on truthiness — return numbers.

null in user comparators

null passed as a normal element is compared by your comparator if you have one. The default-string behavior is what makes the [1, null, 5, 2, undefined] output surprising.

Stability matters

Since ES2019, sort is guaranteed stable:

js
const items = [{id:1,p:1},{id:2,p:1},{id:3,p:2}];
items.sort((a, b) => a.p - b.p);
// id order preserved within same priority: [1, 2, 3]

You can chain sorts: sort by secondary, then by primary, and the secondary order is preserved within ties.

In-place vs immutable

Array.prototype.sort mutates. ES2023 added immutable variants:

  • Array.prototype.toSorted(cmp) — returns a new array.
  • Same for toReversed, toSpliced, with.

Custom keys / localeCompare

js
strs.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));

localeCompare handles accents and locale rules properly.

Complexity

TimSort is O(n log n) but adaptive: a nearly-sorted array runs close to O(n). For very small arrays, the engine uses insertion sort internally.

Interview framing

"Modern engines use TimSort — stable, O(n log n), adaptive. The default comparator is string, not numeric — so [10, 1, 2].sort() is [1, 10, 2] because '1' < '10' < '2' lexicographically. undefined elements are always moved to the end. So [1, null, 5, 2, undefined].sort() ends up [1, 2, 5, null, undefined]. Always pass a numeric comparator ((a,b) => a - b) for numbers. Since ES2019 sort is guaranteed stable, so you can chain sorts. ES2023 added toSorted for the immutable variant."

Follow-up questions

  • What does it mean for sort to be stable?
  • When would you use toSorted?
  • How do you sort by multiple keys?

Common mistakes

  • Default sort on numbers.
  • Comparator returning booleans.
  • Sorting in place when you needed a copy.

Performance considerations

  • TimSort is O(n log n) worst case, O(n) on sorted/partially-sorted input. Insertion sort for small chunks. localeCompare is slower than direct comparison.

Edge cases

  • undefined always at end.
  • NaN behavior in comparators (always returns false in comparisons).
  • Mixed-type arrays.

Real-world examples

  • Sorting product lists by price, table column sort, search relevance sorts.

Senior engineer discussion

Seniors mention TimSort, stability guarantees since ES2019, and the new immutable variants (toSorted). They'd never write `.sort()` on numbers without a comparator.

Related questions