# How to write JavaScript sort comparators

> Learn how JavaScript sort comparators work: the return value contract, the string sort trap, multi-key sorting, localeCompare and Intl.Collator.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-07-30 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/javascript-sort-comparators/

Every time you call `sort()` with a callback, you're writing a **comparator**. It's a tiny function with a strict contract, and getting it slightly wrong produces sorts that look right on your test data and break on real data.

I showed the basics in [how to sort an array of objects by a property](https://flaviocopes.com/how-to-sort-array-of-objects-by-property-javascript/). This post goes deeper into the comparator itself.

## The contract

A comparator takes two elements, `a` and `b`, and returns a number:

- **negative** → `a` comes first
- **positive** → `b` comes first
- **zero** → keep them as they are

That's the whole contract. The engine calls your function many times while sorting, and it trusts those three cases to be consistent.

For numbers, subtraction gives you all three cases for free:

```js
const scores = [92, 88, 105, 61]
scores.sort((a, b) => a - b) //[61, 88, 92, 105]
```

If `a` is smaller, `a - b` is negative, so `a` goes first. Ascending order.

Flip the operands for descending:

```js
scores.sort((a, b) => b - a) //[105, 92, 88, 61]
```

## The default sort trap

Here's the trap everyone falls into once. What does `sort()` do with no comparator?

```js
const scores = [92, 88, 105, 61]
scores.sort() //[105, 61, 88, 92]
```

That's not a bug. Without a comparator, `sort()` converts every element to a **string** and compares the strings. And `'105'` comes before `'61'` alphabetically, because `'1' < '6'`.

My advice: never call `sort()` without a comparator unless you're sorting strings and you know it.

Also notice that `sort()` mutates the array in place. If you want a sorted copy, use `toSorted()`:

```js
const sorted = scores.toSorted((a, b) => a - b)
```

## Sorting strings properly

For strings, the comparison operators technically work:

```js
names.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
```

But this compares character codes, so all uppercase letters sort before all lowercase ones, and accented characters end up in weird places. `'Zoe' < 'alice'` is true.

`localeCompare()` fixes this. It compares strings the way a human would, and it already returns negative/zero/positive, so it slots straight into the comparator:

```js
const names = ['alice', 'Bob', 'carol', 'Ándrea']
names.sort((a, b) => a.localeCompare(b))
//['alice', 'Ándrea', 'Bob', 'carol']
```

I wrote about [localeCompare()](https://flaviocopes.com/javascript-string-localecompare/) in more detail, but the options argument deserves a mention here. The `numeric` option is the one I use most:

```js
const files = ['file10.txt', 'file2.txt', 'file1.txt']

files.sort((a, b) => a.localeCompare(b))
//['file1.txt', 'file10.txt', 'file2.txt']

files.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
//['file1.txt', 'file2.txt', 'file10.txt']
```

Without `numeric: true`, `file10` sorts before `file2` because it's compared character by character. With it, the digits are compared as numbers.

There's also `sensitivity: 'base'`, which ignores case and accents, so `'alice'` and `'Alice'` compare as equal.

## Multi-key sorting

Real sorting is usually "by score descending, then by name". The pattern is: compare the first key, and if it's a tie, fall through to the next key.

```js
const players = [
  { name: 'carol', score: 88 },
  { name: 'alice', score: 92 },
  { name: 'bob', score: 88 },
]

players.sort((a, b) => {
  const byScore = b.score - a.score
  if (byScore !== 0) return byScore
  return a.name.localeCompare(b.name)
})
```

Each key returns early if it decides the order. Only ties reach the next comparison. You can chain as many keys as you want this way.

A common shortcut uses `||`, since `0` is falsy:

```js
players.sort((a, b) =>
  b.score - a.score || a.name.localeCompare(b.name)
)
```

It reads nicely for two or three keys. Beyond that, I prefer the explicit version.

Dates fit the same pattern, because subtracting dates gives you milliseconds. I showed that in [how to sort an array by date](https://flaviocopes.com/how-to-sort-array-by-date-javascript/).

## Stability: ties keep their order

Since ES2019, `sort()` is guaranteed to be **stable**: elements that compare as equal keep their original relative order.

This matters more than it sounds. It means you can sort in passes. Sort by name first, then sort by score, and players with equal scores stay alphabetical from the first pass. It also means a comparator that returns `0` for ties is safe — the engine won't shuffle them randomly.

## Intl.Collator for performance

`localeCompare()` has a cost: with a locale and options, each call does locale work. In a sort over a big array, your comparator runs thousands of times.

`Intl.Collator` lets you pay that setup cost once:

```js
const collator = new Intl.Collator('en', { numeric: true })

files.sort(collator.compare)
```

`collator.compare` is a ready-made comparator function. Same results as `localeCompare()` with the same options, noticeably faster on large arrays. For a few dozen items you won't notice a difference, so don't reach for it until you need it.

## Watch out for null

If some objects are missing the property you're sorting by, `undefined - 5` is `NaN`, and returning `NaN` from a comparator breaks the contract. The sort result becomes unpredictable.

Handle missing values explicitly, deciding whether they go first or last:

```js
players.sort((a, b) => {
  if (a.score == null && b.score == null) return 0
  if (a.score == null) return 1 //nulls last
  if (b.score == null) return -1
  return a.score - b.score
})
```

## Try it interactively

Writing a multi-key comparator with null handling and locale options is exactly the kind of code you write once and copy forever. I built a [sort comparator builder](https://flaviocopes.com/tools/sort-comparator/) where you pick your keys, types, directions and null handling, see the sorted result live on your own JSON, and copy the generated comparator function.
