I’m trying to sort a JavaScript array but the results aren’t coming out in the order I expect. I’ve tried using the default sort() and a custom compare function, but numbers and strings still seem mixed up or out of place. Can someone explain the right way to sort arrays in JavaScript, including numeric and alphabetical sorting, and point out common mistakes I might be making?
JavaScript sort bites a lot of people. The default sort is lexicographic, not numeric. So:
[1, 2, 10].sort()
// result: [1, 10, 2]
It converts elements to strings, then compares ‘1’, ‘10’, ‘2’.
You need a compare function for numbers:
const nums = [5, 1, 20, 3, 10];
nums.sort((a, b) => a - b); // ascending
nums.sort((a, b) => b - a); // descending
That works only if your array is numbers. If some are strings that look like numbers, you get weird stuff.
Example with mixed types:
const arr = [10, ‘2’, 3, ‘11’];
arr.sort();
// [‘10’, ‘11’, ‘2’, 3] // conversions mess things up
Better to normalize first:
// convert all numeric-like strings to numbers
const arr = [10, ‘2’, 3, ‘11’];
const normalized = arr.map(v =>
typeof v === ‘string’ && v.trim() !== ‘’ && !isNaN(v)
? Number(v)
: v
);
normalized.sort((a, b) => {
if (typeof a === ‘number’ && typeof b === ‘number’) return a - b;
return String(a).localeCompare(String(b));
});
Or, if you want strict numeric sorting and you know everything should be a number, convert everything:
const arr = [10, ‘2’, ‘11’, 3];
const sorted = arr
.map(v => Number(v))
.sort((a, b) => a - b);
If you sort strings that represent numbers with default sort, you always get lexicographic order:
[‘1’, ‘2’, ‘10’, ‘20’].sort();
// [‘1’, ‘10’, ‘2’, ‘20’]
For strings that are not numbers, use:
strings.sort((a, b) => a.localeCompare(b));
For objects:
const users = [
{ name: ‘Bob’, age: 30 },
{ name: ‘Alice’, age: 25 },
{ name: ‘Carol’, age: 35 }
];
// by age
users.sort((a, b) => a.age - b.age);
// by name
users.sort((a, b) => a.name.localeCompare(b.name));
Common gotchas you are likely hitting:
- Mixing numbers and strings in the same array.
- Forgetting to return 0 when elements are equal in complex comparators.
- Mutating the original array. sort works in place.
If your compare function looks like this, it is wrong:
arr.sort((a, b) => {
if (a > b) return 1;
if (a < b) return -1;
});
You need the equal case:
arr.sort((a, b) => {
if (a > b) return 1;
if (a < b) return -1;
return 0;
});
Or stick to the shorter numeric one when dealing with pure numbers:
arr.sort((a, b) => a - b);
If your data is mixed and messy, best approach is:
- Decide the type you want for sorting.
- Map everything to that type in a separate array.
- Sort with a clear comparator.
- If needed, keep track of original values with an index:
const arr = [10, ‘2’, 3, ‘11’];
const decorated = arr.map((value, index) => ({
value,
sortKey: Number(value),
index
}));
decorated.sort((a, b) => a.sortKey - b.sortKey);
const sorted = decorated.map(item => item.value);
That pattern keeps behavior predictable, even with strange mixes.
JavaScript’s sort is one of those APIs that looks simple and then quietly ruins your afternoon.
@nachtdromer already covered the basics of lexicographic default sorting and the classic a - b numeric comparator, so I’ll try not to repeat that. Let me focus on why you’re still seeing weird mixes and what patterns actually keep things sane.
1. Understand what sort expects from your compare function
The compare function must obey this contract:
- return
< 0ifashould come beforeb - return
> 0ifashould come afterb - return
0if they’re equal in order
If your comparator sometimes returns undefined, or mixes types oddly, engines can behave inconsistently. For example, this is subtly broken:
arr.sort((a, b) => {
if (a == null) return -1; // puts null/undefined first
if (b == null) return 1;
if (a > b) return 1;
if (a < b) return -1;
// forgot equal case!
});
Many people assume the missing return 0 is fine. It often “works” in small tests, then falls apart with bigger inputs. Always handle equality explicitly for nontrivial comparators.
2. Mixed numbers / strings: decide one rule and stick to it
Big source of pain: arrays like this:
const arr = [10, '2', 3, '11', 'abc', null];
The mistake is trying to let sort “figure it out.” It won’t. You have to define:
- What is the primary ordering? Numeric, alphabetic, or “by type”?
- Where do non numeric values go?
Example: “Sort numerically where possible, then put non numeric stuff at the end alphabetically.”
const arr = [10, '2', 3, '11', 'abc', null, '7x'];
arr.sort((a, b) => {
const aNum = Number(a);
const bNum = Number(b);
const aIsNum = !Number.isNaN(aNum) && a !== null;
const bIsNum = !Number.isNaN(bNum) && b !== null;
// 1. Numeric values first
if (aIsNum && !bIsNum) return -1;
if (!aIsNum && bIsNum) return 1;
// 2. When both numeric, compare numerically
if (aIsNum && bIsNum) return aNum - bNum;
// 3. Fallback: both non numeric, compare as strings
return String(a).localeCompare(String(b));
});
Once you write the rule like that, you stop being surprised by results, because you defined the priority.
I slightly disagree with @nachtdromer on one practical point: I usually do not mutate values into numbers up front if I care about preserving original formatting (like '007' vs '7'). I prefer computing a sortKey inside the comparator or using a decorate/sort/undecorate pattern.
3. Use “decorate → sort → undecorate” for complex rules
If your sorting logic is more than a couple of lines, inline comparators get unreadable and buggy. Use the Schwartzian transform pattern:
const arr = [10, '2', 3, '11', 'abc', null];
const decorated = arr.map((v, i) => ({
original: v,
index: i,
sortKey: !Number.isNaN(Number(v)) && v !== null
? { type: 0, num: Number(v) } // numeric group
: { type: 1, str: String(v) } // non numeric group
}));
decorated.sort((a, b) => {
// First by type (0 before 1)
if (a.sortKey.type !== b.sortKey.type) {
return a.sortKey.type - b.sortKey.type;
}
// Within numeric group
if (a.sortKey.type === 0) {
return a.sortKey.num - b.sortKey.num;
}
// Within string group
const s = a.sortKey.str.localeCompare(b.sortKey.str);
if (s !== 0) return s;
// Optional: stable tie breaker by original index
return a.index - b.index;
});
const sorted = decorated.map(d => d.original);
This looks verbose, but it gives you:
- Clear grouping rules
- Predictable behavior
- Easy debugging (
console.log(decorated)and inspectsortKey)
4. Beware of “hidden” string conversions
One more thing that bites people: values are sometimes already strings before you see them (API responses, form values, etc.). You think you have numbers, but you actually have '1', '2', '10'.
Even this comparator:
arr.sort((a, b) => a - b);
will “work” for '1' and '2' because - forces numeric conversion, but if some element is 'abc', that becomes NaN and all bets are off. So if there’s any chance of junk:
arr.sort((a, b) => {
const aNum = Number(a);
const bNum = Number(b);
if (Number.isNaN(aNum) && Number.isNaN(bNum)) {
return String(a).localeCompare(String(b));
}
if (Number.isNaN(aNum)) return 1; // send non numeric to the end
if (Number.isNaN(bNum)) return -1;
return aNum - bNum;
});
Not pretty, but at least it is explicit.
5. If stuff still looks “random,” check these
Quick checklist:
-
Are you mixing types in the same array?
If yes, define a clear rule: numeric first, alphabetical first, or by type groups. -
Does your compare function always return a number in all branches?
Log it or temporarily throw if it returnsundefined. -
Are you assuming
sortis stable?
Modern engines mostly are, but old ones were not. If you rely on stability between equal keys, add a secondary tie breaker like original index. -
Are you sorting the same array multiple times with different comparators?
That can produce results that feel inconsistent. Work on a copy if needed:const sorted = [...arr].sort(...);
TL;DR: stop trusting the default sort, stop mixing “magic” numbers and strings without rules, and treat your comparator like a tiny spec for how ordering should work. Once you’re explicit about that, the weirdness usually disappears.