How to Sort Arrays in JavaScript: Complete Guide
Master JavaScript array sorting with the sort() method. Covers default lexicographic behavior, custom compare functions, sorting objects by property, stable sort, locale-aware sorting, and common pitfalls.
The sort() method rearranges the elements of an array in place and returns the sorted array. By default, it converts every element to a string and sorts lexicographically (dictionary order). This default behavior is the source of JavaScript's most infamous sorting bug: numbers sort incorrectly without a custom compare function. This guide covers every aspect of sort(), from the default behavior to advanced patterns for sorting objects, dates, and locale-aware text.
Default Sort Behavior
Without a compare function, sort() converts elements to strings and orders them by Unicode code point:
const fruits = ["banana", "cherry", "apple", "date"];
fruits.sort();
console.log(fruits); // ["apple", "banana", "cherry", "date"]
// Looks fine for strings. But watch numbers:
const numbers = [10, 9, 80, 1, 100, 21];
numbers.sort();
console.log(numbers); // [1, 10, 100, 21, 80, 9] — WRONG!The numbers sort as strings: "1" < "10" < "100" < "21" < "80" < "9". This is correct lexicographic order but wrong numerical order.
Numbers Sort Wrong by Default
Calling sort() on a number array without a compare function produces wrong results. The numbers are converted to strings and sorted alphabetically. Always pass a compare function when sorting numbers.
The Compare Function
The compare function tells sort() how to order any two elements:
array.sort(function (a, b) {
// Return negative: a comes first
// Return zero: order unchanged
// Return positive: b comes first
})| Return Value | Meaning |
|---|---|
| Negative number | a should come before b |
0 | a and b are equal (keep original order) |
| Positive number | b should come before a |
Sorting Numbers Correctly
const numbers = [10, 9, 80, 1, 100, 21];
// Ascending
numbers.sort((a, b) => a - b);
console.log(numbers); // [1, 9, 10, 21, 80, 100]
// Descending
numbers.sort((a, b) => b - a);
console.log(numbers); // [100, 80, 21, 10, 9, 1]The a - b pattern works because it returns negative when a < b, zero when equal, and positive when a > b. For a deeper dive into numeric sorting patterns, see sorting numbers correctly.
Sorting Strings
Case-Sensitive (Default)
const words = ["banana", "Apple", "cherry", "apricot"];
words.sort();
console.log(words); // ["Apple", "apricot", "banana", "cherry"]
// Uppercase letters sort before lowercase (A=65, a=97 in Unicode)Case-Insensitive
const words = ["banana", "Apple", "cherry", "apricot"];
words.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(words); // ["Apple", "apricot", "banana", "cherry"]Locale-Aware Sorting with localeCompare
Different languages have different sorting rules. localeCompare() handles accented characters and language-specific ordering:
const names = ["Zürich", "Aarhus", "Ångström", "Amsterdam"];
// Default sort: Unicode order (wrong for many languages)
console.log([...names].sort());
// ["Amsterdam", "Aarhus", "Zürich", "Ångström"]
// Locale-aware: respects language rules
console.log([...names].sort((a, b) => a.localeCompare(b, "en")));
// ["Aarhus", "Amsterdam", "Ångström", "Zürich"]| Approach | Handles Accents | Handles Case | Performance |
|---|---|---|---|
Default sort() | No (Unicode points) | No (uppercase first) | Fast |
localeCompare() | Yes | Yes (configurable) | Slower |
Intl.Collator | Yes | Yes (configurable) | Fastest for repeated sorts |
Using Intl.Collator for Performance
For sorting large arrays of strings, Intl.Collator creates a reusable comparator that is significantly faster than calling localeCompare() per element:
const collator = new Intl.Collator("en", { sensitivity: "base" });
const cities = ["Zürich", "Aarhus", "Ångström", "Amsterdam", "Berlin"];
cities.sort(collator.compare);
console.log(cities); // ["Aarhus", "Amsterdam", "Ångström", "Berlin", "Zürich"]Sorting Objects by Property
Sorting arrays of objects is the most common real-world use case. Extract the property and compare:
By String Property
const employees = [
{ name: "Carol", department: "Engineering" },
{ name: "Alice", department: "Marketing" },
{ name: "Bob", department: "Engineering" },
];
employees.sort((a, b) => a.name.localeCompare(b.name));
console.log(employees.map(e => e.name)); // ["Alice", "Bob", "Carol"]By Numeric Property
const products = [
{ name: "Keyboard", price: 129 },
{ name: "Mouse", price: 49 },
{ name: "Monitor", price: 349 },
{ name: "Cable", price: 12 },
];
// Ascending by price
products.sort((a, b) => a.price - b.price);
console.log(products.map(p => `${p.name}: $${p.price}`));
// ["Cable: $12", "Mouse: $49", "Keyboard: $129", "Monitor: $349"]By Date
const events = [
{ title: "Launch", date: "2026-03-15" },
{ title: "Beta", date: "2026-01-10" },
{ title: "Alpha", date: "2025-11-20" },
{ title: "GA", date: "2026-06-01" },
];
events.sort((a, b) => new Date(a.date) - new Date(b.date));
console.log(events.map(e => e.title)); // ["Alpha", "Beta", "Launch", "GA"]Multi-Key Sort (Primary + Secondary)
const students = [
{ name: "Alice", grade: "A", gpa: 3.9 },
{ name: "Bob", grade: "B", gpa: 3.2 },
{ name: "Carol", grade: "A", gpa: 3.7 },
{ name: "Dave", grade: "B", gpa: 3.5 },
{ name: "Eve", grade: "A", gpa: 3.9 },
];
// Sort by grade (A first), then by GPA descending
students.sort((a, b) => {
const gradeCompare = a.grade.localeCompare(b.grade);
if (gradeCompare !== 0) return gradeCompare;
return b.gpa - a.gpa; // Higher GPA first within same grade
});
console.log(students.map(s => `${s.name} (${s.grade}, ${s.gpa})`));
// ["Alice (A, 3.9)", "Eve (A, 3.9)", "Carol (A, 3.7)", "Dave (B, 3.5)", "Bob (B, 3.2)"]sort() Mutates the Original Array
Unlike map() or filter(), sort() modifies the array in place:
const original = [3, 1, 4, 1, 5, 9];
const sorted = original.sort((a, b) => a - b);
console.log(original); // [1, 1, 3, 4, 5, 9] — MODIFIED
console.log(sorted === original); // true — same referenceSorting Without Mutation
To preserve the original array, copy it first using the spread operator or toSorted() (ES2023):
const original = [3, 1, 4, 1, 5, 9];
// Option 1: spread + sort
const sorted1 = [...original].sort((a, b) => a - b);
// Option 2: toSorted() (ES2023) — non-mutating
const sorted2 = original.toSorted((a, b) => a - b);
console.log(original); // [3, 1, 4, 1, 5, 9] — unchanged
console.log(sorted1); // [1, 1, 3, 4, 5, 9]
console.log(sorted2); // [1, 1, 3, 4, 5, 9]toSorted() Is the Modern Alternative
ES2023 introduced toSorted(), which returns a new sorted array without modifying the original. It is the immutable counterpart to sort(), ideal for React state, Redux reducers, and any pattern where mutation is unwanted.
Stable Sort
JavaScript's sort() is guaranteed stable since ES2019. This means elements that compare as equal maintain their original relative order:
const items = [
{ name: "Widget A", category: "tools" },
{ name: "Widget B", category: "tools" },
{ name: "Gadget A", category: "electronics" },
{ name: "Gadget B", category: "electronics" },
];
items.sort((a, b) => a.category.localeCompare(b.category));
// Stable: within "electronics", Gadget A still comes before Gadget B
console.log(items.map(i => i.name));
// ["Gadget A", "Gadget B", "Widget A", "Widget B"]reverse() and toReversed()
To reverse an array's order (not sort in reverse):
const letters = ["a", "b", "c", "d"];
// Mutating
letters.reverse();
console.log(letters); // ["d", "c", "b", "a"]
// Non-mutating (ES2023)
const original = ["a", "b", "c", "d"];
const reversed = original.toReversed();
console.log(original); // ["a", "b", "c", "d"] — unchanged
console.log(reversed); // ["d", "c", "b", "a"]Common Mistakes
Sorting numbers without a compare function:
const prices = [99, 5, 250, 10, 1000];
// Bug: lexicographic sort
prices.sort();
console.log(prices); // [10, 1000, 250, 5, 99]
// Fix: numeric compare
prices.sort((a, b) => a - b);
console.log(prices); // [5, 10, 99, 250, 1000]Forgetting sort() mutates:
const original = [5, 3, 8, 1];
// Bug: original is now sorted too
function getSorted(arr) {
return arr.sort((a, b) => a - b);
}
const sorted = getSorted(original);
console.log(original); // [1, 3, 5, 8] — mutated!
// Fix: copy before sorting
function getSortedSafe(arr) {
return [...arr].sort((a, b) => a - b);
}Inconsistent compare functions:
const items = [{ val: 3 }, { val: 1 }, { val: 2 }];
// Bug: returning only true/false instead of negative/zero/positive
items.sort((a, b) => a.val > b.val);
// Unreliable! true coerces to 1, false to 0 — no negative value
// Fix: proper numeric comparison
items.sort((a, b) => a.val - b.val);Using sort() for shuffling:
const arr = [1, 2, 3, 4, 5];
// Bug: biased shuffle
arr.sort(() => Math.random() - 0.5);
// Fix: Fisher-Yates shuffle
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}Best Practices
- Always pass a compare function for numbers. Never rely on the default lexicographic sort for numeric data.
- Copy before sorting when you need to preserve the original. Use
[...arr].sort()ortoSorted(). - Use localeCompare() for strings when accent sensitivity or language-specific ordering matters.
- Use Intl.Collator for large datasets. It is significantly faster than
localeCompare()for arrays with thousands of strings. - Keep compare functions pure. They should only compare, not produce side effects.
Rune AI
Key Insights
- Default sort is lexicographic: always pass a compare function for numbers
- sort() mutates in place: use spread or toSorted() to preserve the original array
- Compare function returns negative, zero, or positive: not true/false
- Stable since ES2019: equal elements keep their original relative order
- Use Intl.Collator for large string datasets: faster than repeated localeCompare() calls
Frequently Asked Questions
Does sort() modify the original array?
Why do numbers sort incorrectly without a compare function?
Is JavaScript sort stable?
What is the time complexity of sort()?
What is the difference between sort() and toSorted()?
Conclusion
The sort() method is JavaScript's built-in tool for ordering array elements, but its default lexicographic behavior is a constant source of bugs for numeric data. The essential pattern is to always pass a compare function ((a, b) => a - b for numbers, localeCompare() for strings) and to copy the array before sorting when the original order must be preserved. With ES2023's toSorted() providing a non-mutating alternative, modern JavaScript now covers both the in-place and immutable sorting needs for every use case.
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.