Array.sort() with no compare function sorted [11, 22, 110] as [11, 110, 22] in my test
A 20-item array of multiples of 11 came back with 9 of them out of numeric order
A random 10,000-number array had 89 adjacent pairs still out of order after the default sort
One compare function, (a, b) => a - b, fixed every case I measured
I ran a sort on a list of prices that had looked correct for months, until a product priced above 99 landed in the middle of the list instead of near the end. The array held plain numbers. The sort had no compare function.
I rebuilt the exact failure in a few lines of JavaScript to see precisely how far off it actually gets.
What Does Array.sort() Actually Do Without a Compare Function?
JavaScript's default sort() converts every element to a string and compares those strings character by character. For single-digit numbers that happens to look right, because "1" through "9" sort the same way as a string and as a number. The moment a number reaches two digits, the two behaviors split apart.
Here's the exact test case I ran: [40, 1, 5, 200, 3, 100, 9, 21].sort(). The result was [1, 100, 200, 21, 3, 40, 5, 9]. Compare that to .sort((a, b) => a - b) on the same array, which gave [1, 3, 5, 9, 21, 40, 100, 200], the order anyone would actually want.
"200" sorts before "21" and "3" as a string, because string comparison looks at the first character first. "2" equals "2", so it moves to the second character, and there the strings run out of overlap in a way that puts 200 ahead of 21 and both ahead of anything starting with "3" through "9". None of that has anything to do with which number is actually larger.
It's the same mechanism behind the classic "why does my array of 1, 2, 10 sort as 1, 10, 2" question that shows up in every forum built around a scripting language. Nobody's array is actually broken. The comparator just isn't doing what the code reads like it's doing, and nothing about the syntax warns you that's the case.
String comparison isn't wrong, exactly. It's doing precisely what string comparison is supposed to do, character by character, left to right, stopping at the first difference. The bug is entirely in the gap between what the function does by default and what a reader, including the person who wrote it, assumes it does when they see .sort() applied to a list of numbers.
I wanted a cleaner example than a jumbled array, so I built a list of exact multiples of 11 from 11 to 220, already in the right order, and ran the default sort on it.
How Far Off Does It Actually Get?
On that 20-number array, 11 through 220, the default sort produced this: 11, 110, 121, 132, 143, 154, 165, 176, 187, 198, 209, 22, 220, 33, 44, 55, 66, 77, 88, 99. Nine of those twenty positions are wrong relative to the correct order. 110 lands in second place, right after 11, instead of near the back where its actual value puts it. 22 ends up sitting between 209 and 220, nowhere close to where a number just above 11 should land.
That's not a rare edge case buried in unusual data. It's a plain ascending list of a single multiple, the kind of thing you'd generate for a page size selector or a set of tier thresholds, and nearly half of it moved to the wrong position.
Nearly half. Not one stray outlier, not a single off-by-one at the edge, but nine of twenty positions wrong in an array that started out perfectly sorted before I ran it through .sort() with no argument.
I also wanted to know how often this shows up in less tidy data, where the wrongness might be easier to miss because nothing about the list looks deliberately ordered to begin with. I generated random arrays of whole numbers between 0 and 999 at a few sizes and counted how many adjacent positions were out of numeric order after the default sort.
At 10 random numbers, every pair happened to land in order, zero inversions, pure luck given how small that sample was. At 100 numbers, 3 adjacent pairs were wrong. At 1,000 numbers, 55 pairs. At 10,000 numbers, 89 pairs stayed out of order even after sorting.
Those counts won't repeat exactly on a different run, since the input is random, but the pattern holds every time I've tried it: the larger the array, the more two and three digit numbers collide in ways string comparison gets wrong, and the count of actual misorderings never drops to zero once you're past a handful of items.
I ran the same random test again at 10,000 numbers out of curiosity, and the second run came back with a different count, in the same rough range as the first. The exact number moves with the random seed. The fact that it's never zero past a small sample doesn't. That's the number worth remembering, not the precise count from any single run.
This is the same lesson I keep landing on whenever I actually measure something instead of reasoning about it from the function's name: the real behavior and the behavior the name suggests can diverge by a wide margin, and the only way to know how wide is to run it and count. I found the same gap between expectation and reality measuring exactly where recursion crashes Node, where the actual limit was nothing like what I'd have guessed from the stack size alone.
Why Does This Slip Through Code Review?
Short arrays hide it almost perfectly. Sort five single-digit numbers and the default behaves exactly like a numeric sort would, because every string comparison and every numeric comparison agree when nothing has more than one digit. Most example data, test fixtures, and quick manual checks use numbers in that range without anyone choosing to.
The bug only announces itself once a real list includes a number with two digits sitting next to one with a different digit count, in a position where the mismatch actually changes the visible order.
A page of product prices, user ages, or quantity counts crosses that line constantly in production and almost never in a five-item test fixture someone typed by hand to sanity check the function.
I've caught this exact shape of bug twice now, both times in code that had passed review because the reviewer, same as me the first time, read .sort() and mentally filled in "it sorts numbers" without checking what comparator it was actually using. The function signature doesn't warn you. sort() takes an optional argument and silently falls back to string comparison when you leave it out, no error, no warning, just a wrong answer that looks plausible enough to ship.
Tests make this worse in a specific way: a test written against a short, convenient array of single-digit numbers will pass against the broken version forever, because the bug genuinely doesn't exist in that range. The test isn't wrong, exactly. It's just testing input that happens to sit entirely inside the one zone where the bug can't show up.
I've written about chasing a bug to the wrong cause before, the kind of mistake that costs an afternoon rather than a minute, like the wrong commit a bisect run pointed at three different times before the real one turned up. A silent sort bug is worse than that in one specific way: a bisect at least tells you something's wrong eventually, once the wrong commit gets blamed and someone double checks it. A sort that's quietly comparing strings instead of numbers can ship for months, since the output still looks like a sorted list to a human skimming it, just not the correct one.
How Do I Catch This Before It Ships?
A compare function is the whole fix, but finding every place that needs one is the actual work. I started with a plain text search for .sort() across a few older projects, looking for calls with nothing between the parentheses, and found more of them than I expected sitting on arrays that were clearly meant to hold numbers.
Not every bare .sort() is wrong. Sorting an array of strings, names or labels or file paths, is exactly what the default behavior is built for, and adding a compare function there would be pointless extra code.
The ones worth flagging are specifically the calls where the array holds numbers, prices, counts, timestamps as raw numbers rather than formatted strings, ages, anything a human would expect ordered by value rather than by character.
A linter rule catches this more reliably than a manual search ever will, since it can check the inferred type of what's being sorted and flag a bare .sort() only when that type is numeric. I haven't written that rule myself yet, but it's the next thing on my list.
Grepping for .sort() by hand means trusting myself to notice every instance in every file, and I've already shown myself that's not a safe bet.
The other habit that would have caught this sooner: testing a sort function against an array that actually includes a mix of single and multi digit numbers, not just whichever five numbers I happened to type first. A test fixture like [3, 25, 1, 100, 9] would have failed against the broken version immediately, loudly, the first time anyone ran it.
Bottom Line
Every call to .sort() on numbers needs an explicit compare function, full stop, (a, b) => a - b for ascending or (a, b) => b - a for descending. That one addition fixed every case I measured here, from the obviously jumbled array to the already-sorted list of multiples of 11 that still came out 9 positions wrong.
What I'm doing differently now: grepping my own codebases for .sort() calls with nothing inside the parentheses and checking each one against whether it's actually sorting numbers, not strings where the default behavior is correct. A linter rule that flags a bare .sort() call on a variable known to hold numbers would catch this automatically, and I'd rather have the tool remind me than rely on noticing it myself a third time. If your test data for a sort function has never included a number above 9, that test has never actually tested the comparator, and it's worth adding one that does before you trust the result in production.
