sort() is the JavaScript array method most likely to surprise you the first time you use it with numbers. The behavior is not a bug. It is the result of a specific design decision that made sense in the context of the original language spec. Understanding why it works this way makes the fix obvious and prevents the mistake from recurring.
The Surprise
[10, 9, 100, 2, 55].sort();
// [10, 100, 2, 55, 9]
If you expected [2, 9, 10, 55, 100], you were expecting numeric sort order. What you got was lexicographic sort order. The array was sorted as if the numbers were strings: "10" comes before "100" because "1" comes before "1" and then "0" comes before "00"... actually "100" starts with "1", "10" starts with "1", and then comparing "0" vs nothing makes "10" come before "100". "2" comes last because "2" > "1" lexicographically.
This is not specific to numbers. .sort() with no argument converts each element to a string and sorts by Unicode code point order. For the 26 uppercase ASCII letters, this is alphabetical order. For numbers, it produces a result that looks like a mistake.
Why It Works This Way
The original JavaScript specification chose string comparison as the default because arrays could contain any type and string comparison works for the most basic case: sorting an array of strings alphabetically. The language spec defines sort()'s default behavior in terms of string conversion, which is consistently applied across types.
This is documented in the ECMAScript specification at TC39, which governs how JavaScript array methods behave. The choice is defensible: it is consistent and predictable if you know the rule. The problem is that the rule surprises developers who expect numeric comparison by default.
The Fix: The Comparison Function
sort() accepts an optional comparison function. The function receives two elements, a and b, and should return:
- A negative number if a should come before b
- A positive number if b should come before a
- Zero if they are equal
For numeric ascending sort:
[10, 9, 100, 2, 55].sort((a, b) => a - b);
// [2, 9, 10, 55, 100]
a - b is negative when a is less than b (a should come first), positive when a is greater (b should come first), and zero when they are equal. This is the standard idiom for numeric sort.
For numeric descending:
[10, 9, 100, 2, 55].sort((a, b) => b - a);
// [100, 55, 10, 9, 2]
Sorting Objects
For arrays of objects, the comparison function accesses the field you want to sort by.
const users = [
{ name: "Carol", age: 28 },
{ name: "Alice", age: 34 },
{ name: "Bob", age: 22 }
];
// Sort by age ascending:
users.sort((a, b) => a.age - b.age);
// [Bob(22), Carol(28), Alice(34)]
// Sort by name alphabetically:
users.sort((a, b) => a.name.localeCompare(b.name));
// [Alice, Bob, Carol]
localeCompare() handles international characters correctly and is the right choice for string fields. The simple < and > comparison works for ASCII strings but produces incorrect results for accented characters and other Unicode ranges.
The In-Place Modification Issue
sort() modifies the original array. This surprises developers who expect the same behavior as map() and filter(), which produce new arrays.
const original = [3, 1, 2];
const sorted = original.sort();
console.log(original); // [1, 2, 3] -- modified
console.log(original === sorted); // true -- same array
If you need the original order preserved, copy the array before sorting.
const sorted = [...original].sort((a, b) => a - b);
// or
const sorted = original.slice().sort((a, b) => a - b);
The spread [...arr] creates a shallow copy. slice() without arguments does the same. Both work; spread is more idiomatic in modern JavaScript.
Sort Stability
A sort algorithm is stable if elements that compare as equal maintain their original relative order. As of ES2019, the ECMAScript specification requires sort() to be stable. Modern JavaScript engines (V8, SpiderMonkey, JavaScriptCore) all implement stable sort.
If you are targeting environments that predate ES2019, stable sort is not guaranteed. In practice, most environments in active use today guarantee stability. The V8 blog covered the V8 engine's switch to a stable sort algorithm when this change was made.
Multi-Field Sorting
When you want to sort by multiple fields (primary and secondary), combine comparisons:
users.sort((a, b) => {
// Primary: department
const deptCompare = a.department.localeCompare(b.department);
if (deptCompare !== 0) return deptCompare;
// Secondary: age within department
return a.age - b.age;
});
If the primary comparison produces a non-zero result, that determines the order. If it is zero (same department), the secondary comparison applies.
Practical Summary
The three things to remember about sort():
- Default sort is lexicographic (string comparison). Always provide a comparison function for numbers.
-
sort()modifies the original array. Copy first if you need the original. - The comparison function returns negative (a first), positive (b first), or zero (equal).
The guide at 137Foundry on JavaScript array methods covers sort() alongside the complete array method reference. The MDN Web Docs document the full sort algorithm and comparison function specification. The Node.js documentation covers how the V8 engine's sort implementation behaves in server-side contexts.
137Foundry builds and maintains JavaScript applications for clients where data manipulation patterns like these come up in every project. The JavaScript Array specification section at tc39.es is the canonical source for how sort() and all other array methods are defined.
Practical Patterns for Real Sort Scenarios
Sort by date (most recent first):
const events = [
{ name: "Launch", date: "2026-03-15" },
{ name: "Review", date: "2026-05-01" },
{ name: "Kickoff", date: "2026-01-10" }
];
events.sort((a, b) => new Date(b.date) - new Date(a.date));
// [Review(2026-05-01), Launch(2026-03-15), Kickoff(2026-01-10)]
new Date() produces a Date object. Subtracting two Date objects produces a number (milliseconds difference), which the comparison function uses correctly.
Sort with null values at the end:
items.sort((a, b) => {
if (a.value == null && b.value == null) return 0;
if (a.value == null) return 1;
if (b.value == null) return -1;
return a.value - b.value;
});
Handling nulls explicitly prevents NaN results from null - someNumber and ensures nulls land consistently at the end.
Sort a copy, preserve the original:
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
// items is unchanged; sorted is the ordered copy
This pattern is the default choice when the original order matters elsewhere in the component or function.
Why the Comparison Function Matters
The comparison function is not a boolean test. It is a three-way comparison that defines a total order over the elements. A function that returns only 0 or 1 (never negative) will produce incorrect results on many engines because the algorithm depends on all three outcomes. The a - b idiom works because arithmetic subtraction naturally produces all three: negative, zero, and positive.
Understanding the contract of the comparison function, along with the full set of array methods, is covered in the JavaScript array methods guide at 137Foundry. The Wikipedia article on sorting algorithms provides background on why comparison-based sorting requires a total order and what makes a comparison function correct.
Top comments (0)