TidyURLs
Go to Tools
BLOG
The TidyURLsJournal

Practical guides, tutorials, and insights about URLs, SEO, web development, and online marketing. Learn, troubleshoot, and get the most of the web.

Back to the Journal
TidyURLs Journal

Why JavaScript Array.sort() Gives the Wrong Numeric Order

Practical insights, clear explanations, and useful ideas for understanding the web.

October 26, 20265 min read5 topics
Why JavaScript Array.sort() Gives the Wrong Numeric Order
Explore the details
#JavaScript#Array.sort#Array methods#Sorting#toSorted
October 26, 20265 min read

JavaScript’s Array.prototype.sort() can put numbers in an order that looks wrong because its default behavior sorts values as strings. This troubleshooting guide shows how to sort numbers ascending or descending, avoid changing the original array, and use toSorted() when your runtime supports it.

Key Takeaways

  • Without a comparator, sort() compares string representations, so numbers are ordered lexicographically rather than numerically.
  • Use (a, b) => a - b for ascending numeric order and (a, b) => b - a for descending order.
  • sort() changes the array it is called on. Use toSorted() or copy the array first when you need to preserve the original.
  • Check support for toSorted() in your target browsers and runtimes; transpiling syntax alone does not add missing built-in methods.

Why the default sort looks wrong

With no comparator, JavaScript converts array elements to strings and sorts those strings by their UTF-16 code units. That is useful for some text lists, but it is not a numeric comparison.

const values = [1, 30, 4, 21, 100000];

console.log(values.sort());
// [1, 100000, 21, 30, 4]

For example, the string "100000" comes before "21" because its first character, 1, comes before 2. The values are not compared by their mathematical size.

Sort numbers with a comparator

A comparator receives two elements, a and b. Return a negative number when a should come first, a positive number when b should come first, or zero when they should be treated as equal.

For ordinary finite JavaScript numbers, subtracting the values provides that comparison:

const values = [1, 30, 4, 21, 100000];

const ascending = [...values].sort((a, b) => a - b);
const descending = [...values].sort((a, b) => b - a);

console.log(ascending);
// [1, 4, 21, 30, 100000]

console.log(descending);
// [100000, 30, 21, 4, 1]

The examples copy values before sorting so both results leave the source array unchanged. If mutation is intended, call values.sort((a, b) => a - b) directly.

When values may not be numbers

The subtraction comparator assumes values can be compared as numbers. Numeric strings such as "12" are coerced by subtraction, but arbitrary strings produce NaN, which is not a meaningful numeric ordering. Validate or normalize input before sorting if data can be malformed.

NaN also needs an explicit policy if it can appear in the array. For instance, to put NaN values last while sorting numbers ascending:

const compareNumbersNaNLast = (a, b) => {
  const aIsNaN = Number.isNaN(a);
  const bIsNaN = Number.isNaN(b);

  if (aIsNaN && bIsNaN) return 0;
  if (aIsNaN) return 1;
  if (bIsNaN) return -1;
  return a - b;
};

console.log([8, NaN, 3].toSorted(compareNumbersNaNLast));
// [3, 8, NaN] (where toSorted is supported)

For arrays containing undefined, sort() places those values after defined values and does not pass them to the comparator. Sparse-array holes are placed at the end as well. If those cases matter, decide their desired position as part of the data-handling logic.

sort() changes the original array

sort() sorts in place and returns the same array, not a new independently sorted array. This can surprise code that shares an array with another function or component.

const scores = [40, 10, 30];
const result = scores.sort((a, b) => a - b);

console.log(scores); // [10, 30, 40]
console.log(result === scores); // true

To keep the original array intact, make a shallow copy before calling sort():

const scores = [40, 10, 30];
const sortedScores = [...scores].sort((a, b) => a - b);

console.log(sortedScores); // [10, 30, 40]
console.log(scores); // [40, 10, 30]

This copies the array, not the objects inside it. If the array contains objects, the sorted copy still refers to the same objects.

Use toSorted() for a non-mutating sort

toSorted() returns a new array and leaves the original unchanged. It accepts the same kind of comparator as sort():

const scores = [40, 10, 30];

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

console.log(ascending); // [10, 30, 40]
console.log(descending); // [40, 30, 10]
console.log(scores); // [40, 10, 30]

toSorted() is an ECMAScript 2023 addition. Support depends on the browser or JavaScript runtime version, so check the MDN browser compatibility information for your deployment targets. A build tool that transforms newer JavaScript syntax does not necessarily provide missing built-in methods; use a supported runtime, an appropriate polyfill, or [...array].sort(compare) as a fallback.

Common Mistakes

  • Calling sort() without a comparator for numbers: this invokes the default string-based ordering.
  • Writing (a, b) => a > b: a comparator should return a negative number, zero, or a positive number. A boolean does not express the comparison contract correctly.
  • Assuming sort() returns a separate array: it returns the same array after sorting it in place.
  • Using subtraction on unvalidated data: nonnumeric values can make the comparator return NaN, so define how malformed values should be handled.
  • Assuming every runtime supports toSorted(): check your target environments and provide a fallback if needed.

Best Practices

  • Make the intended ordering explicit with a comparator.
  • Copy the array or use toSorted() when callers must retain the original order.
  • Keep the comparator consistent: for the same pair of values, it should give the same ordering result.
  • Validate or normalize incoming values before sorting, especially when data comes from forms, APIs, or CSV files.
  • Test representative edge cases, including duplicates, negative values, decimal values, and any NaN or missing values your application allows.

Conclusion

The default sort() order is string-based, which explains why numeric arrays can appear scrambled. Supply a numeric comparator for ascending or descending order, and choose sort(), a copied array, or toSorted() according to whether the original array should change.

References