Sorting Numbers Correctly in JS Arrays Tutorial
Learn why JavaScript sorts numbers wrong by default and how to fix it. Covers numeric compare functions, ascending/descending order, sorting floats, handling NaN and Infinity, sorting mixed data, and performance tips.
JavaScript's sort() method converts every element to a string before comparing, which means [10, 9, 80] sorts to [10, 80, 9] by default. This is the single most common JavaScript sorting mistake, and it affects every developer who calls sort() on a numeric array without a compare function. This tutorial covers exactly why it happens, how to fix it, and every edge case you will encounter when sorting numbers in JavaScript.
Why Numbers Sort Wrong by Default
The default sort() behavior converts each element to a string, then compares those strings by Unicode code points:
const prices = [100, 25, 3, 50, 10, 200];
prices.sort();
console.log(prices); // [10, 100, 200, 25, 3, 50]The string comparisons happen character by character:
| Number | As String | First Character | Unicode Code Point |
|---|---|---|---|
| 100 | "100" | "1" | 49 |
| 25 | "25" | "2" | 50 |
| 3 | "3" | "3" | 51 |
| 50 | "50" | "5" | 53 |
| 10 | "10" | "1" | 49 |
| 200 | "200" | "2" | 50 |
Since "1" < "2" < "3" < "5", the numbers sort as strings: all numbers starting with "1" come first, then "2", then "3", then "5". This produces completely wrong numerical ordering.
Never Sort Numbers Without a Compare Function
Calling array.sort() on numbers without (a, b) => a - b is a bug in 100% of cases. There is no scenario where lexicographic ordering of integers or floats produces correct numeric results.
The Correct Way to Sort Numbers
Pass a compare function that subtracts one number from the other:
const prices = [100, 25, 3, 50, 10, 200];
// Ascending order
prices.sort((a, b) => a - b);
console.log(prices); // [3, 10, 25, 50, 100, 200]
// Descending order
prices.sort((a, b) => b - a);
console.log(prices); // [200, 100, 50, 25, 10, 3]How a - b Works
The compare function must return:
- A negative number if
ashould come beforeb - Zero if they are equal
- A positive number if
bshould come beforea
// When a=10, b=25: 10 - 25 = -15 (negative → 10 before 25) ✓
// When a=50, b=25: 50 - 25 = 25 (positive → 25 before 50) ✓
// When a=25, b=25: 25 - 25 = 0 (equal → keep original order) ✓| Compare Result | Meaning | Example |
|---|---|---|
a - b < 0 | a comes first | 10 - 25 = -15 |
a - b === 0 | Equal, keep order | 25 - 25 = 0 |
a - b > 0 | b comes first | 50 - 25 = 25 |
Sorting Floating-Point Numbers
The a - b pattern works identically for decimals:
const measurements = [3.14, 2.718, 1.414, 0.577, 2.236];
measurements.sort((a, b) => a - b);
console.log(measurements); // [0.577, 1.414, 2.236, 2.718, 3.14]Sorting Currency Values
const transactions = [99.99, 0.50, 149.95, 10.00, 5.99, 249.99];
// Ascending
transactions.sort((a, b) => a - b);
console.log(transactions); // [0.5, 5.99, 10, 99.99, 149.95, 249.99]Handling Special Number Values
NaN and Infinity
NaN breaks the compare function because any arithmetic with NaN returns NaN, and sort() treats NaN comparisons as equal to everything, producing unpredictable placement:
const data = [5, NaN, 3, 1, NaN, 4];
// Unpredictable: NaN contaminates comparisons
data.sort((a, b) => a - b);
console.log(data); // Order of NaN is implementation-dependentFilter out NaN before sorting:
const data = [5, NaN, 3, 1, NaN, 4];
const clean = data.filter(n => !Number.isNaN(n));
clean.sort((a, b) => a - b);
console.log(clean); // [1, 3, 4, 5]Or push NaN values to the end:
const data = [5, NaN, 3, 1, NaN, 4];
data.sort((a, b) => {
if (Number.isNaN(a)) return 1; // a goes to end
if (Number.isNaN(b)) return -1; // b goes to end
return a - b;
});
console.log(data); // [1, 3, 4, 5, NaN, NaN]Infinity Values
Infinity and -Infinity work correctly with a - b without special handling:
const values = [100, Infinity, -Infinity, 0, 50];
values.sort((a, b) => a - b);
console.log(values); // [-Infinity, 0, 50, 100, Infinity]Sorting Object Arrays by Numeric Property
The most common real-world scenario is sorting objects by a number field:
const products = [
{ name: "Keyboard", price: 129.99, rating: 4.5 },
{ name: "Mouse", price: 49.99, rating: 4.8 },
{ name: "Monitor", price: 349.99, rating: 4.2 },
{ name: "Webcam", price: 79.99, rating: 3.9 },
{ name: "Headphones", price: 199.99, rating: 4.7 },
];
// By price ascending
products.sort((a, b) => a.price - b.price);
console.log(products.map(p => `${p.name}: $${p.price}`));
// ["Mouse: $49.99", "Webcam: $79.99", "Keyboard: $129.99", ...]
// By rating descending (highest first)
products.sort((a, b) => b.rating - a.rating);
console.log(products.map(p => `${p.name}: ${p.rating}`));
// ["Mouse: 4.8", "Headphones: 4.7", "Keyboard: 4.5", ...]Multi-Key Numeric Sort
Sort by one numeric field, then break ties with another:
const scores = [
{ name: "Alice", points: 95, time: 120 },
{ name: "Bob", points: 88, time: 95 },
{ name: "Carol", points: 95, time: 105 },
{ name: "Dave", points: 88, time: 110 },
];
// Sort by points descending, then by time ascending (faster is better)
scores.sort((a, b) => {
if (b.points !== a.points) return b.points - a.points;
return a.time - b.time;
});
console.log(scores.map(s => `${s.name}: ${s.points}pts, ${s.time}s`));
// ["Carol: 95pts, 105s", "Alice: 95pts, 120s", "Bob: 88pts, 95s", "Dave: 88pts, 110s"]Sorting Without Mutation
Like all array sorting, sort() mutates the original. Copy first when needed:
const original = [42, 7, 19, 3, 88];
// Spread + sort
const ascending = [...original].sort((a, b) => a - b);
// toSorted() (ES2023)
const descending = original.toSorted((a, b) => b - a);
console.log(original); // [42, 7, 19, 3, 88] — unchanged
console.log(ascending); // [3, 7, 19, 42, 88]
console.log(descending); // [88, 42, 19, 7, 3]Sorting Numeric Strings
When your data contains numbers stored as strings (common with form inputs and CSV parsing), convert before comparing:
const inputValues = ["100", "25", "3", "50", "10"];
// Bug: still sorts as strings
inputValues.sort();
console.log(inputValues); // ["10", "100", "25", "3", "50"]
// Fix: parse inside the compare function
inputValues.sort((a, b) => Number(a) - Number(b));
console.log(inputValues); // ["3", "10", "25", "50", "100"]Mixed Numbers and Numeric Strings
const mixed = [30, "5", 100, "20", 8, "15"];
mixed.sort((a, b) => Number(a) - Number(b));
console.log(mixed); // ["5", 8, "15", "20", 30, 100]Performance Tips
The built-in sort() uses TimSort (O(n log n)) in most engines. The callback runs O(n log n) times, so keep it cheap:
// Slow: parsing dates inside compare (runs n log n times)
events.sort((a, b) => new Date(a.date) - new Date(b.date));
// Fast: pre-compute timestamps, sort, then map back
const withTimestamps = events.map(e => ({
...e,
_ts: new Date(e.date).getTime(),
}));
withTimestamps.sort((a, b) => a._ts - b._ts);| Array Size | sort() Calls | Tip |
|---|---|---|
| < 100 | ~500 | Any compare function is fine |
| 100 - 10,000 | ~10K - 130K | Avoid expensive operations in compare |
| 10,000+ | 130K+ | Pre-compute sort keys, use typed arrays |
Typed Arrays Sort Faster
For pure numeric sorting of large datasets (50K+ elements), consider using Float64Array or Int32Array. Their sort() method uses native numeric comparison by default, skipping the compare function overhead entirely.
Common Mistakes
Using sort() without compare for numbers:
// This is ALWAYS a bug for numeric arrays
[10, 2, 30].sort(); // [10, 2, 30] → wrong!
[10, 2, 30].sort((a, b) => a - b); // [2, 10, 30] → correctReturning boolean instead of number:
const nums = [3, 1, 2];
// Bug: true (1) and false (0) — no negative value possible
nums.sort((a, b) => a > b);
// May produce wrong results because 0 means "equal"
// Fix: subtraction returns negative, zero, or positive
nums.sort((a, b) => a - b);Mutating state during sort:
const data = [5, 3, 8, 1];
// Bug: modifying state while sorting
let comparisons = 0;
data.sort((a, b) => {
comparisons++;
data.push(0); // Never do this!
return a - b;
});Best Practices
- Always use
(a, b) => a - bfor numeric ascending sort. Make it a reflex. - Copy before sorting when the original order matters. Use
[...arr].sort()ortoSorted(). - Filter NaN before sorting. NaN values produce unpredictable results with any compare function.
- Pre-compute expensive sort keys. If the compare function parses dates, computes string lengths, or does regex matching, extract those values first.
- Use forEach() or map() to inspect sorted results without re-sorting.
Rune AI
Key Insights
- Default sort is string-based: never omit the compare function for numeric arrays
- Use (a, b) => a - b for ascending: swap to b - a for descending
- Filter NaN values first: NaN breaks comparison logic unpredictably
- Copy before sorting: use spread or toSorted() to preserve the original array
- Pre-compute sort keys: extract expensive values before sorting large datasets
Frequently Asked Questions
Why does JavaScript sort numbers as strings by default?
Is there a built-in method for numeric sort?
How do I sort numbers in descending order?
What happens if my array has both numbers and undefined?
Can I sort a very large array of numbers efficiently?
Conclusion
Numeric sorting in JavaScript requires exactly one thing: a compare function. The (a, b) => a - b pattern handles ascending integers, floats, currency values, and object properties. The default sort() behavior (lexicographic) is never correct for numbers. By combining numeric comparators with NaN filtering, pre-computed sort keys for expensive operations, and toSorted() for immutable patterns, you can sort any numeric dataset correctly and efficiently. The rule is simple: if it contains numbers, always pass a compare function to sort().
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.