Knowledge Base/guides/Why JavaScript sort() Mutates Arrays and How to Avoid It

Why JavaScript sort() Mutates Arrays and How to Avoid It

JavaScript sort() mutates arrays because it is an in-place array method. It reorders the array you call it on and returns that same array reference. That is not a small trivia point. It is the difference between "show me these items sorted" and "change the source data for everyone else using it."

Here is the proof:

const scores = [30, 10, 20];

const sorted = scores.sort((a, b) => a - b);

console.log(sorted); // [10, 20, 30]
console.log(scores); // [10, 20, 30]
console.log(sorted === scores); // true

The assignment did not protect the original array. sorted and scores point at the same array.

The bug: assigning does not copy

This looks like a non-mutating sort:

const products = [
  { name: "Notebook", price: 7 },
  { name: "Monitor", price: 199 },
  { name: "Mouse", price: 29 },
];

const cheapestFirst = products.sort((a, b) => a.price - b.price);

The variable name cheapestFirst sounds new. The array is not new.

console.log(products.map((product) => product.name));
// ["Notebook", "Mouse", "Monitor"]

That matters when some other part of the program still expects the original order.

function renderFeaturedProducts(products) {
  const cheapestFirst = products.sort((a, b) => a.price - b.price);

  renderPriceTable(cheapestFirst);
  renderOriginalCarousel(products);
}

By the time renderOriginalCarousel runs, products is already sorted by price. The carousel did not ask for that. It just got dragged into the decision because sort() changed the shared array.

Why sort works this way

The most useful answer is not philosophical: the method is specified to sort in place. JavaScript arrays are mutable objects, and sort() belongs to the older family of mutating array methods alongside push, pop, splice, and reverse.

In-place sorting can also avoid allocating a second array. That can be useful when you intentionally own the array and do not need the old order.

The surprise comes from comparing sort() with methods like map and filter.

const numbers = [3, 1, 2];

const doubled = numbers.map((n) => n * 2);

console.log(doubled); // [6, 2, 4]
console.log(numbers); // [3, 1, 2]

map returns a new array. sort returns the same array. Same dot syntax, different contract. The JavaScript Array Methods guide covers that output-shape distinction in more detail.

Use toSorted when you can

Modern JavaScript has toSorted(), the copying version of sort().

const scores = [30, 10, 20];

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

console.log(sorted); // [10, 20, 30]
console.log(scores); // [30, 10, 20]

If your runtime supports it, toSorted() is the clearest option. The method name says what happens: make a sorted copy.

The compare function works the same way it does with sort():

const newestFirst = posts.toSorted(
  (a, b) => b.publishedAt - a.publishedAt,
);

Use toSorted() when the original order still matters, especially in UI code, state updates, selectors, and data transformations.

Copy first when toSorted is not available

If you need to support an environment without toSorted(), copy the array before sorting it.

const scores = [30, 10, 20];

const sorted = [...scores].sort((a, b) => a - b);

console.log(sorted); // [10, 20, 30]
console.log(scores); // [30, 10, 20]

Spread creates a shallow copy. Then sort() mutates that copy.

You can do the same thing with slice():

const sorted = scores.slice().sort((a, b) => a - b);

Both patterns are common. I prefer spread for short arrays because the copy is visually obvious. slice() is still useful when you are already working in older JavaScript style or want to avoid newer syntax.

For a broader explanation of spread and shallow copies, see The Spread Operator.

Shallow copies still share objects

Copying the array protects the order of the original array. It does not clone every object inside it.

const users = [
  { name: "Ada", score: 90 },
  { name: "Grace", score: 95 },
];

const byScore = [...users].sort((a, b) => b.score - a.score);

byScore[0].score = 100;

console.log(users[1].score); // 100

The array is new. The user objects inside it are the same objects. That is what "shallow copy" means.

If all you need is a sorted display order, a shallow array copy is enough. If you also plan to edit the objects, create new objects too:

const editableUsers = users
  .map((user) => ({ ...user }))
  .sort((a, b) => b.score - a.score);

Now the array is new and each user object is new.

Do not forget the compare function

The default sort() behavior converts values to strings and compares those strings. That is why this happens:

const numbers = [1, 20, 3, 100];

console.log(numbers.toSorted());
// [1, 100, 20, 3]

That is correct according to the default rule, and deeply unhelpful for numeric sorting. Use a compare function:

const ascending = numbers.toSorted((a, b) => a - b);
const descending = numbers.toSorted((a, b) => b - a);

console.log(ascending); // [1, 3, 20, 100]
console.log(descending); // [100, 20, 3, 1]

For strings, the default can be okay for simple ASCII-like lists:

const names = ["Grace", "Ada", "Linus"];

console.log(names.toSorted());
// ["Ada", "Grace", "Linus"]

For user-facing text, especially mixed case or accented characters, use localeCompare:

const sortedNames = names.toSorted((a, b) =>
  a.localeCompare(b, undefined, { sensitivity: "base" }),
);

Sorting is already enough of a footgun. Do not add string-number confusion for free.

React state and sorted data

Mutation hurts most when an array is shared. React state is a common example.

function ProductList({ products }) {
  const cheapestFirst = products.sort((a, b) => a.price - b.price);

  return cheapestFirst.map((product) => (
    <ProductCard key={product.id} product={product} />
  ));
}

That component mutates its products prop. Props are supposed to be inputs, not scratch pads.

Use a copying sort:

function ProductList({ products }) {
  const cheapestFirst = products.toSorted(
    (a, b) => a.price - b.price,
  );

  return cheapestFirst.map((product) => (
    <ProductCard key={product.id} product={product} />
  ));
}

Or, if you need the fallback:

const cheapestFirst = [...products].sort(
  (a, b) => a.price - b.price,
);

This keeps rendering code from rewriting the data it receives. Polite code, finally.

When mutation is fine

Mutation is not automatically wrong. If you created the array as a local scratch value and no other code needs its old order, sort() is fine.

const visibleScores = scores.filter((score) => score.visible);

visibleScores.sort((a, b) => b.value - a.value);

return visibleScores;

That array was created by filter, so sorting it does not mutate scores. You own the temporary array.

The rule is simple: mutate when you own the array and the old order does not matter. Copy when the array came from somewhere else or another part of the code may still use it.

A practical sorting helper

If your code sorts the same shape in several places, make the non-mutating behavior part of the helper.

function sortByPrice(products) {
  return products.toSorted((a, b) => a.price - b.price);
}

function sortByPriceFallback(products) {
  return [...products].sort((a, b) => a.price - b.price);
}

Then callers do not have to remember the mutation detail every time.

const cheapestFirst = sortByPrice(products);

Small helpers like this are not glamorous. They are cheaper than debugging one accidental reorder across three components.

Key Takeaways

  • sort() mutates the array it is called on and returns that same array.
  • Assigning the result of sort() to a new variable does not copy the array.
  • Use toSorted() for a sorted copy when your runtime supports it.
  • Use [...array].sort(...) or array.slice().sort(...) as a fallback.
  • Copying the array is shallow; objects inside the array are still shared.
Share this article:
javascriptarraysmutation