The thing that always trips people up
You see this in someone's code:
numbers.sort((a, b) => b - a);
And you think:
- "Why subtract?"
- "Why
b - aand nota - b?" - "What does this even return?"
Three minutes from now you will never forget it.
How .sort() actually works
Array.sort() reorders an array. To do that, it needs to compare two items at a time and decide which one goes first. You teach it how by giving it a compare function.
The contract is one rule:
compareFn(a, b) returns:
a NEGATIVE number → a comes first
a POSITIVE number → b comes first
ZERO → leave them in their current order
That is it. The function returns a number; the sign is the only thing that matters.
The cute mental model
Think of the compare function as a referee judging a match between two contestants, a and b. After the match, the referee gives a score. The sign of the score is the verdict:
-
Negative score →
awins →amoves to the front. -
Positive score →
bwins →bmoves to the front. - Zero → tie → nothing moves.
That's the whole rule. The function returns a number; the sign decides who gets the front spot.
An important caveat about the contract
A good comparator must be consistent: if you swap the arguments, you must get the opposite sign. That is, compareFn(a, b) and compareFn(b, a) must disagree (one positive, one negative — or both zero if equal).
What you should not do is return a constant like -1 regardless of arguments:
[10, 2].sort((a, b) => -1); // ❌ tells the engine "a first" no matter what a is
That makes no sense as a comparison ("a is always less than b, even when a and b are swapped"). The engine doesn't error — it just trusts you — and the result becomes a quirk of which order the engine happened to call your function. Don't write comparators like that. Always make the answer depend on a and b.
The good news: subtraction is naturally consistent. a - b and b - a are mirror images, so it works no matter which order the engine calls the comparator. That is why the next section is about subtraction.
Why subtraction?
Subtraction is a quick trick to produce that signed number.
a - b is negative ⇒ a is smaller ⇒ a comes first
a - b is positive ⇒ a is bigger ⇒ b comes first
a - b is zero ⇒ equal ⇒ keep order
If you want ascending (small to big), use a - b.
If you want descending (big to small), flip it: b - a.
Rule of thumb: the variable on the left of the subtraction is the one that ends up at the small end of the sorted array.
a - b→ais on the left → small values land at the start (ascending).b - a→bis on the left → small values land at the end (descending).
A worked example
Sort [3, 1, 4, 1, 5, 9, 2, 6] in ascending order:
[3, 1, 4, 1, 5, 9, 2, 6].sort((a, b) => a - b);
// [1, 1, 2, 3, 4, 5, 6, 9]
Descending order:
[3, 1, 4, 1, 5, 9, 2, 6].sort((a, b) => b - a);
// [9, 6, 5, 4, 3, 2, 1, 1]
Same numbers, opposite directions, one character changed in the compare function.
What happens if you forget the compare function
[10, 2, 100, 4].sort();
// [10, 100, 2, 4] ← what?!
.sort() with no arguments converts every element to a string and sorts alphabetically. "10" comes before "2" because the character '1' is alphabetically smaller than '2'. Same for "100" — '1' < '2', so "100" appears before "2" even though numerically 100 is much bigger.
Lesson: always pass a compare function for numbers.
Sorting objects by a field
This is where the trick really earns its keep. Imagine an array of workers:
const workers = [
{ name: "Alice", shifts: 3 },
{ name: "Bob", shifts: 9 },
{ name: "Carol", shifts: 5 },
];
Most shifts first (descending):
workers.sort((a, b) => b.shifts - a.shifts);
// [
// { name: "Bob", shifts: 9 },
// { name: "Carol", shifts: 5 },
// { name: "Alice", shifts: 3 },
// ]
Least shifts first (ascending):
workers.sort((a, b) => a.shifts - b.shifts);
// [
// { name: "Alice", shifts: 3 },
// { name: "Carol", shifts: 5 },
// { name: "Bob", shifts: 9 },
// ]
Notice: you only change the order inside the subtraction. The structure stays identical.
Sorting strings (where subtraction won't work)
Subtraction is for numbers. Strings need localeCompare:
const names = ["Carol", "Alice", "Bob"];
names.sort((a, b) => a.localeCompare(b));
// ["Alice", "Bob", "Carol"]
localeCompare returns negative / zero / positive following the same contract.
One little gotcha — sort mutates the original
const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);
original; // [1, 2, 3] ← changed!
sorted; // [1, 2, 3] ← same reference
If you want a sorted copy without touching the original, use toSorted (modern browsers/Node 20+) or copy first:
const sorted = [...original].sort((a, b) => a - b);
// original stays [3, 1, 2]
Cheat sheet to pin on your wall
ascending (small to big): (a, b) => a - b
descending (big to small): (a, b) => b - a
ascending by field: (a, b) => a.field - b.field
descending by field: (a, b) => b.field - a.field
strings ascending: (a, b) => a.localeCompare(b)
strings descending: (a, b) => b.localeCompare(a)
TL;DR
.sort() calls your compare function on pairs of elements. The function returns a signed number:
- Negative →
acomes first. - Positive →
bcomes first. - Zero → leave them as-is.
Subtraction is a tiny trick to produce that signed number, and the order of the operands controls the direction.
a - b= small first (ascending).
b - a= big first (descending).
Now you will never wonder again.
Top comments (0)