What algorithm does Array.prototype.sort() use? What’s the output of [1, null, 5, 2, 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.
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:
- Skip
undefinedelements; move them to the end. - Convert remaining to strings.
- Compare strings lexicographically (UTF-16 code units).
So:
[1, null, 5, 2, undefined].sort();
// String sort: "1" < "2" < "5" < "null"; undefined at end
// → [1, 2, 5, null, undefined][10, 1, 2].sort();
// → [1, 10, 2] — "1" < "10" < "2" lexicographicallyThis is the classic interview trick. Always pass a comparator for numbers:
[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:
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
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.