ESC

Type to search the knowledge base.

Sorting Arrays Correctly

Array.sort mutates, default string order breaks numbers — stable sort, compare functions, localeCompare, and immutable patterns.

beginner3 min read
  • javascript
  • sorting-arrays

Array.prototype.sort is one of the most footgun-heavy APIs in JS interviews. Default compare converts to strings. It mutates in place. Modern engines use a stable sort, but you should still write an explicit comparator for anything non-trivial.

[10, 2, 1].sort();
// [1, 10, 2]  — lexicographic: '10' < '2'

Numeric and general compare

nums.sort((a, b) => a - b); // ascending
nums.sort((a, b) => b - a); // descending

// comparator contract:
// < 0 → a before b
// > 0 → a after b
// 0   → keep relative order (stable)

Don’t return booleans (a > b) — coerced to 0/1, wrong for ascending.

Strings and locale

names.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));

// numeric option for dotted versions / house numbers
['item2', 'item10', 'item1'].sort((a, b) =>
  a.localeCompare(b, undefined, { numeric: true }),
);
// item1, item2, item10

Objects

users.sort((a, b) => a.lastName.localeCompare(b.lastName)
  || a.firstName.localeCompare(b.firstName));

Multi-key: first non-zero comparison wins.

Immutable sort (React state)

// BAD — mutates existing array reference’s contents
state.items.sort(...);

// GOOD
const sorted = [...state.items].sort((a, b) => a.price - b.price);
setState({ ...state, items: sorted });

// modern
const sorted2 = state.items.toSorted((a, b) => a.price - b.price);

toSorted / toReversed (ES2023) return copies; check Baseline if you skip polyfills.

Stability

const rows = [
  { name: 'b', order: 1 },
  { name: 'a', order: 2 },
  { name: 'b', order: 3 },
];
rows.sort((a, b) => a.name.localeCompare(b.name));
// both 'b' rows keep relative order (order 1 then 3) on modern engines

Stability is required by the ECMAScript spec now; still don’t rely on old IE myths in interviews — mention it briefly.

Mixed types and NaN

[1, NaN, 2].sort((a, b) => a - b); // NaN comparisons are messy

Normalize data before sort: filter nulls, coerce prices with Number, decide where empties go.

function comparePrice(a, b) {
  const pa = a.price ?? Infinity;
  const pb = b.price ?? Infinity;
  return pa - pb;
}

Interview answer (out loud)

“Default sort is string order and mutates the array. For numbers I use (a,b)=>a-b; for strings localeCompare; for immutable UI state I copy first or use toSorted. Comparators return negative, zero, or positive — not booleans. Modern JS sort is stable.”

Schwartian transform for expensive keys

function sortByExpensive(arr, keyFn) {
  return arr
    .map((item, i) => ({ item, i, k: keyFn(item) }))
    .sort((a, b) => (a.k < b.k ? -1 : a.k > b.k ? 1 : a.i - b.i))
    .map(({ item }) => item);
}

Compute the sort key once per element when keyFn is heavy (normalize strings, parse dates).

Dates and ISO strings

events.sort((a, b) => Date.parse(a.iso) - Date.parse(b.iso));
// or store epoch ms numbers to avoid reparse

Invalid dates yield NaN comparisons — filter or push invalids to the end explicitly.

Locale-aware names

names.sort((a, b) => a.localeCompare(b, navigator.language));

Hard-coding 'en' may be wrong for international products; pass the active UI locale.

Further reading

Related guides