Array.sort() Trap: The Median That Lies About Big Numbers2026-05-14
This function is supposed to compute the median of a list of numbers. It passed every test the junior engineer wrote — until production hit it with real telemetry data and dashboards started showing impossible latencies.
function findMedian(numbers) {
if (numbers.length === 0) return null;
const sorted = [...numbers].sort();
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[mid - 1] + sorted[mid]) / 2;
}
return sorted[mid];
}
// Tests that "pass":
console.log(findMedian([3, 1, 4, 1, 5, 9, 2, 6])); // 3.5 ✓
console.log(findMedian([7, 2, 8, 5, 1])); // 5 ✓
// Production:
console.log(findMedian([50, 200, 75, 1000, 9])); // 75? No — returns 75 ✓
console.log(findMedian([50, 200, 75, 1000, 9, 3])); // returns 137.5, real median is 62.5
console.log(findMedian([2, 10, 100, 1000])); // returns 55, real median is 55 ✓ (lucky)
console.log(findMedian([2, 11, 100, 1000])); // returns 55.5, real median is 55.5 ✓ (lucky again!)
console.log(findMedian([8, 10, 100, 9])); // returns 54, real median is 9
JavaScript's Array.prototype.sort() — when called without a comparator — sorts elements as strings. It coerces every element with ToString and compares the resulting UTF-16 code unit sequences lexicographically.
So [8, 10, 100, 9].sort() does not give you [8, 9, 10, 100]. It gives you [10, 100, 8, 9], because "10" < "100" < "8" < "9". The median calculation then picks the average of the two middle slots — 100 and 8 — yielding 54 instead of the correct 9.
What makes this bug exquisitely cruel:
findMedian([3,1,4,1,5,9,2,6]), see 3.5, and ship.[10, 20, 30, 40] and [100, 200, 300, 400] both sort "correctly" because all elements have equal digit counts.NaN, not undefined — just a value that's wrong, often by a believable amount. Perfect for corrupting analytics silently.sort(): this is perfectly happy on a number[].The spec mandates this behavior — it's not a quirk of any one engine. It dates back to a time when JavaScript arrays were generic untyped sequences, and "sort everything as strings" was the only universally safe default.
Always pass an explicit numeric comparator:
function findMedian(numbers) {
if (numbers.length === 0) return null;
const sorted = [...numbers].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) {
return (sorted[mid - 1] + sorted[mid]) / 2;
}
return sorted[mid];
}
One subtle caveat: (a, b) => a - b itself can lie if a or b is large enough that the subtraction overflows past Number.MAX_SAFE_INTEGER, or if either is NaN. For untrusted input, prefer (a, b) => a < b ? -1 : a > b ? 1 : 0, which compares directly without arithmetic.
A useful lint rule: ban bare .sort() calls entirely. ESLint's sort-compare rule does exactly this, and it has saved more dashboards than anyone can count.
Array.sort() defaults to lexicographic string comparison — always pass a comparator when sorting numbers, or your tests will pass while production quietly invents wrong answers.
