JavaScript's Array.sort() Trap: The Median That Lies About Big Numbers

2026-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

The Bug

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:

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.

The Fix

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.

Key Takeaway: JavaScript's 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.

All newsletters