Since ES2019, Array.prototype.sort is required to be stable. What exactly does that guarantee, and what can you build on it?
answer
- a promise about ties only
- comparator returns 0 for the pair
- input order preserved for equal elements
- required since ES2019, unspecified before
- enables least-significant-key-first passes
basics
~20 sStability means elements whose comparator returns zero keep the relative order they had in the input. It lets you order by a secondary key in one pass and a primary key in the next, and it makes repeated sorts of the same data reproducible.
solid answer
~50 sStability is a guarantee about **ties only**: whenever the comparator returns `0` for two elements, `Array.prototype.sort` must leave them in the relative order they had before the sort. ES2019 made this a spec requirement; before that, engines were free to use an unstable algorithm, and several switched strategy above a size threshold, so equal elements could be reordered on larger arrays but not smaller ones. The practical payoff is sequential sorting: sort by the least significant key, then by the more significant one, and the earlier ordering survives inside each tie group — which is exactly what a clickable table header needs. It also makes output reproducible for a given input, so snapshot tests and diffs do not churn. What it does not do is rescue an inconsistent comparator: if your comparator is not antisymmetric and transitive, the order is implementation-defined and stability buys you nothing.
code
javascript · 13 linesconst rows = [
{ name: 'Cy', score: 5 },
{ name: 'Ada', score: 9 },
{ name: 'Bo', score: 5 },
{ name: 'Di', score: 9 },
];
const view = [...rows]
.sort((a, b) => a.name.localeCompare(b.name)) // secondary key first
.sort((a, b) => b.score - a.score); // primary key last
console.log(view.map(r => `${r.score} ${r.name}`));
// [ '9 Ada', '9 Di', '5 Bo', '5 Cy' ]go deeper
Know the definition: elements the comparator calls equal keep the order they arrived in. Be able to say that modern JavaScript guarantees this for Array.prototype.sort.
Date the guarantee to ES2019 and describe what came before — unspecified tie order, with engines observably unstable above an internal size threshold. Explain why passes run least-significant key first.
Show where you would still not rely on it: add a unique-key tiebreak when the order crosses a wire, feeds a snapshot test, or must match a server-side ORDER BY that has its own tie rules.
Treat ordering as a contract between client and server. Decide whether sort order is defined by the query, the API response, or the client, and require a deterministic total order so pagination cannot drop or duplicate rows.
## What stability means here A sort is **stable** when elements the comparator declares equivalent — it returns `0` for that pair — come out in the same relative order they went in. It says nothing about elements the comparator can actually distinguish; those are ordered by the comparator, stable or not. ```js const rows = [ { name: 'Ada', score: 5 }, { name: 'Bo', score: 5 }, { name: 'Cy', score: 9 }, ]; rows.sort((a, b) => a.score - b.score); // Ada is guaranteed to still precede Bo: the comparator returns 0 for them. ``` ## The ES2019 change Before ES2019, the specification explicitly left the ordering of equal-comparing elements unspecified. Engines exploited that: it was common to use a simple insertion-style approach for short arrays, which is naturally stable, and switch to a faster but unstable strategy above a threshold. The observable result was a genuinely nasty class of bug — the same code preserved tie order for a list of eight rows and scrambled it for a list of eighty, so it looked fine in development and broke on real data. ES2019 removed that freedom for `Array.prototype.sort`: stability is now required, and current engines comply. If you support environments predating that edition, you cannot rely on it and must add an explicit tiebreak instead. ## What it buys you **Sequential sorting.** Sort by the least significant key first and the most significant key last; each later pass preserves the ordering established by the earlier ones within its tie groups. ```js const view = [...rows] .sort((a, b) => a.name.localeCompare(b.name)) // secondary .sort((a, b) => b.score - a.score); // primary // highest score first; equal scores stay alphabetical ``` This is the natural model for an interactive table: the user clicks "Name", then clicks "Score", and expects rows with the same score to remain alphabetical. Implementing that with a single comparator would require the UI to remember and re-express every previous click as a chained term; leaning on stability, each click is one more `sort` call. **Reproducibility.** With a consistent comparator and a stable sort, the same input array always yields the same output array — no engine-dependent shuffling of ties. That matters for snapshot tests, for rendering lists with keys, and for any diff between two runs. **Cheap partial ordering.** You can group or partition first and refine later without worrying that the refinement destroys the earlier arrangement. ## What it does not buy you - **It does not make an invalid comparator safe.** If the comparator is not consistent — a boolean return, a `NaN`, a random value, a non-transitive rule — the resulting order is implementation-defined. Stability is a promise about ties under a *valid* comparison; it is not a promise about garbage in. - **It says nothing about ordering across different inputs.** Two arrays containing equal elements in different original orders will sort to different outputs. If you need a canonical order regardless of input order, add a final tiebreak on a unique key, such as an id. - **It is not a statement about performance or algorithm.** The spec constrains observable ordering, not the strategy an engine picks. ## The explicit alternative When you want an ordering that does not depend on stability at all, make the comparator a total order by appending a unique tiebreak: ```js const cmp = (a, b) => b.score - a.score || a.id - b.id; ``` Now no two distinct elements ever compare equal, so stability is irrelevant and the result is identical on every engine, in every edition, for any input permutation. This is the defensive choice for data that crosses a wire or lands in a snapshot test. ## How to answer this in an interview Define stability precisely (ties keep input order), date the guarantee (ES2019) and say what preceded it (unspecified, and observably unstable above engine thresholds), give the one killer use case (sequential multi-key sorting for clickable table headers), and then show judgment by noting the limit — it does not repair an inconsistent comparator, and a unique-key tiebreak is stronger when you need a canonical order.
- How would you get a canonical order without relying on stability at all?Make the comparator a total order by appending a tiebreak on a unique field: `(a, b) => b.score - a.score || a.id - b.id`. No two distinct elements then compare equal, so the output is fully determined by the data rather than by input order — identical across engines, editions, and any permutation of the input.
- Does stability protect you from a buggy comparator?No. Stability constrains only what happens to pairs the comparator reports as equal under a consistent comparison. If the comparator is non-transitive, returns booleans, or yields `NaN`, the specification leaves the whole ordering implementation-defined, and no stability guarantee applies to that mess.
Think of re-sorting a hand of playing cards by suit: a stable sort leaves the ranks inside each suit in the order you had already put them, instead of reshuffling them.
saying these in an interview costs you the question
- Defining stability as "the same input always sorts the same"
- Thinking stability affects elements the comparator can distinguish
- Claiming stability makes an inconsistent comparator safe
- Sorting by the primary key first in a multi-pass scheme
- Assuming stability guarantees a particular algorithm or complexity