· 6 min read
Why JavaScript sort() puts 10 before 9, and how to fix it
JavaScript's array sort() orders numbers as strings, so 10 lands before 9. Here's why, the comparator fix, and the a > b trap that sorts nothing.

Call .sort() on an array of numbers in JavaScript and you get [1, 10, 100, 25, 9]. That is not a bug in the engine. With no comparator, Array.prototype.sort converts every element to a string and sorts the strings, so "10" comes before "9" the same way "ab" comes before "b". Pass (a, b) => a - b and the numbers sort numerically.
That is the short version. The longer version has a second trap that catches people who already know the first one: a comparator that returns a > b looks right, passes a quick test, and quietly sorts nothing.
Reproduce it
[10, 9, 1, 100, 25].sort();
// [1, 10, 100, 25, 9]
[-1, -10, 2, -2].sort();
// [-1, -10, -2, 2]
Negative numbers make it more obvious. As strings, "-10" sorts before "-2" because "1" comes before "2", so the order is meaningless as numbers.
Small test arrays hide this. [3, 1, 2].sort() returns [1, 2, 3] because single digits sort the same as strings and as numbers. The bug only appears once values cross a digit boundary, which is usually when real data shows up.
Why does JavaScript sort numbers as strings?
The language spec defines the default comparison for sort as a string comparison. Each element is converted with the same rules as String(x), and the resulting strings are compared by UTF-16 code units. A code unit is the 16-bit number JavaScript uses to store each character of a string, so the comparison is effectively "which character code is smaller", left to right.
sort was designed for mixed arrays where it cannot assume anything about element types. Strings are the one comparison every value supports, so that became the default. It is a sensible fallback for an array of anything and the wrong answer for an array of numbers.
Two details fall out of this:
nullis converted to the string"null", so it lands between"m"and"o"rather than at either end.undefinedis special. It is never passed to your comparator and always goes to the end, along with empty slots in sparse arrays.
[3, undefined, 1, null, 2].sort();
// [1, 2, 3, null, undefined]
Typed arrays are the exception. new Int32Array([10, 9, 1]).sort() returns [1, 9, 10], because typed array sort compares numerically by default. If you move data between a typed array and a plain array, the behavior of a bare .sort() changes with it.
The fix: a numeric comparator
A comparator is a function that takes two elements and returns a number: negative if the first should come first, positive if the second should, and zero if their order does not matter.
[10, 9, 1, 100, 25].sort((a, b) => a - b);
// [1, 9, 10, 25, 100]
[10, 9, 1, 100, 25].sort((a, b) => b - a);
// [100, 25, 10, 9, 1]
The same pattern works for objects by subtracting the field you care about:
items.sort((a, b) => a.price - b.price);
Subtraction only works when both values are ordinary numbers. Two cases need something else:
- BigInt.
a - breturns a BigInt, and the comparator must return a Number, so you get aTypeError. Use explicit comparisons:(a, b) => (a < b ? -1 : a > b ? 1 : 0). - NaN.
NaN - xisNaN, which makes the comparator inconsistent. The spec leaves the result implementation-defined, so filterNaNout or map it toInfinitybefore sorting.
Why does a > b not work as a comparator?
This is the second trap. It reads naturally, and some older engines happened to produce the right order with it on small inputs:
[3, 1, 2].sort((a, b) => a > b);
// [3, 1, 2]
a > b returns a boolean. Booleans convert to 1 or 0, never to a negative number. The sort algorithm asks "should a go before b?" and your comparator can only answer "no" or "they are equal". It never says "yes", so elements that should move earlier stay where they are.
On a longer array nothing moves at all:
const xs = [0, 7, 14, 1, 8, 15, 2, 9, 16, 3];
xs.sort((a, b) => a > b);
// [0, 7, 14, 1, 8, 15, 2, 9, 16, 3], unchanged in current V8
The fix is to return a number with a sign: a - b for numbers, or the three-way ternary above for anything else that supports <.
Sorting strings correctly
The default string sort is also not what most people expect for text, because it compares code units rather than letters. Uppercase letters have lower codes than lowercase, and accented letters come after z:
['b', 'a', 'B', 'é', 'e'].sort();
// ['B', 'a', 'b', 'e', 'é']
['b', 'a', 'B', 'é', 'e'].sort((x, y) => x.localeCompare(y));
// ['a', 'b', 'B', 'e', 'é']
localeCompare uses language-aware rules. For strings with embedded numbers, like file names or version labels, an Intl.Collator with numeric: true compares the digit runs as numbers:
const byName = new Intl.Collator(undefined, { numeric: true }).compare;
['item10', 'item9', 'item1'].sort(byName);
// ['item1', 'item9', 'item10']
Create the collator once and reuse its compare function. Calling localeCompare with options on every comparison builds that machinery over and over on large arrays.
sort mutates; toSorted does not
One more surprise worth pairing with this: sort reorders the array in place and returns the same array, not a copy.
const a = [10, 9, 1];
const b = a.sort();
a === b; // true
In React state or anywhere else that relies on references changing, sorting in place mutates data you did not mean to touch. toSorted was added in ES2023 and returns a new array, leaving the original alone:
const prices = [10, 9, 1];
const sorted = prices.toSorted((a, b) => a - b);
// prices: [10, 9, 1]
// sorted: [1, 9, 10]
It takes the same comparator and has the same string default, so the numeric fix still applies. It is available in current browsers and in Node 20 and later; for older targets, [...arr].sort(fn) does the same job. Both sort and toSorted are stable, meaning elements that compare equal keep their original relative order, which the spec has required since ES2019.
Key takeaways
- A bare
.sort()compares elements as strings, so numbers come out in dictionary order. - Always pass a comparator for numbers:
(a, b) => a - bascending,(a, b) => b - adescending. - A comparator must return a signed number.
a > breturns a boolean and does not sort. - Use
localeCompareorIntl.Collatorfor human-readable text, withnumeric: truefor embedded numbers. sortmutates in place; usetoSortedor a copy when the original must stay intact.
FAQ
Why does [1, 2, 3].sort() look correct?
Single-digit numbers sort the same way as strings and as numbers. The string comparison only diverges once values have different lengths, like 9 and 10.
Does the default sort work for dates?
Not reliably. Date objects convert to long text strings that begin with the weekday, so they sort alphabetically by day name. Use (a, b) => a - b, which subtracts their timestamps.
Is toSorted faster than sort?
No. It does the same work plus a copy. Use it when you need the original array unchanged, not for speed.