Understanding JavaScript Array Sorting: Why sort() Behaves the Way It Does

JavaScript's sort() method produces results that surprise developers who encounter it for the first time with numbers. The behavior is intentional, follows a defined specification, and makes sense once you understand the rule. This piece covers what the rule is, why it exists, and how to write comparison functions that produce the sort order you actually want.

The Unexpected Behavior

Sort a small array of numbers in JavaScript.

[10, 9, 100, 2, 55].sort();

If you expected [2, 9, 10, 55, 100], you expected numeric sort. The actual result is [10, 100, 2, 55, 9]. The array was sorted as if the numbers were text strings. "10" comes before "100" in text order. "2" comes after "100" because "2" > "1" in character comparison.

This is not a bug. The ECMAScript specification defines sort() with no argument as sorting by string conversion and Unicode code point comparison. The behavior is consistent and specified. It just does not match what many developers expect.

Why the Default is String Comparison

JavaScript arrays are heterogeneous. They can hold numbers, strings, objects, null, undefined, or any mix. When the specification was written, choosing a default sort order required a rule that worked across all these types.

String comparison worked for the most obvious use case at the time: sorting arrays of strings alphabetically. The < and > operators on strings compare by Unicode code points in order, which produces alphabetical order for ASCII characters. Applying that rule to numbers produces the "numbers treated as strings" behavior.

The design also means sort() always has a defined output for any array, including mixed-type arrays. A comparison function that assumes numbers would throw or produce undefined results for non-numeric elements. String comparison degrades gracefully.

The practical implication: always supply a comparison function when sorting numbers or objects.

The Comparison Function

sort() accepts a comparison function that takes two elements and determines their relative order.

The return value rules: - Return a negative number: the first argument comes before the second - Return a positive number: the second argument comes before the first - Return zero: the two elements are treated as equal in sort order

For numeric ascending sort, a - b is the standard idiom.

[10, 9, 100, 2, 55].sort((a, b) => a - b);
// [2, 9, 10, 55, 100]

a - b is negative when a is smaller (a should come first), positive when a is larger (b should come first), and zero when they are equal. It is concise and covers the full range of numeric values.

For descending order, reverse the operands.

[10, 9, 100, 2, 55].sort((a, b) => b - a);
// [100, 55, 10, 9, 2]

Sorting Strings Correctly

For strings, localeCompare() is the comparison function to use. It handles accented characters, case, and locale-specific ordering correctly. Direct < and > comparison works for plain ASCII but produces incorrect results for Unicode strings with accented or non-Latin characters.

const cities = ["Zurich", "Amsterdam", "Berlin", "Vienna"];
cities.sort((a, b) => a.localeCompare(b));
// ["Amsterdam", "Berlin", "Vienna", "Zurich"]

// With accented characters:
const names = ["Zoe", "Adam", "Amelie", "Beatrice"];
names.sort((a, b) => a.localeCompare(b, "en", { sensitivity: "base" }));

The second and third arguments to localeCompare() specify the locale and comparison options. The sensitivity: "base" option treats accented and non-accented versions of the same letter as equal, which is often the right behavior for search and display.

Sorting Objects by Field

When sorting arrays of objects, the comparison function accesses the relevant field.

const employees = [
  { name: "Carol", salary: 72000 },
  { name: "Alice", salary: 85000 },
  { name: "Bob", salary: 68000 }
];

// By salary, descending:
employees.sort((a, b) => b.salary - a.salary);
// [Alice(85k), Carol(72k), Bob(68k)]

// By name, ascending:
employees.sort((a, b) => a.name.localeCompare(b.name));
// [Alice, Bob, Carol]

The comparison function has access to the full object, so you can sort by any property or combination of properties.

Multi-Field Sort

When you want a primary sort field and a secondary sort for ties on the primary field, chain the comparisons.

employees.sort((a, b) => {
  const dept = a.department.localeCompare(b.department);
  if (dept !== 0) return dept;
  return a.name.localeCompare(b.name);
});

If the departments are different, that determines the order. If they are the same, names determine the order within the department.

The In-Place Modification Gotcha

sort() modifies the original array and returns it. This is different from map() and filter(), which always produce new arrays.

const original = [3, 1, 2];
const sorted = original.sort((a, b) => a - b);

original === sorted; // true - same array

If you need the original order preserved, copy the array before sorting.

const sortedCopy = [...original].sort((a, b) => a - b);

The spread creates a shallow copy. original.slice() without arguments does the same thing. Either approach works; the spread syntax is more common in modern JavaScript.

Sort Stability

A stable sort preserves the relative order of elements that compare as equal. If two employees have the same salary, a stable sort keeps them in their original order relative to each other.

ECMAScript 2019 made sort stability a formal requirement. All modern JavaScript engines (Chrome, Firefox, Safari, Node.js) implement stable sort. If you are targeting older environments, stability is not guaranteed.

The MDN Web Docs sort documentation includes the full specification behavior and notes on stability. The broader JavaScript array method reference, including map(), filter(), reduce(), and find(), is at 137Foundry's JavaScript array methods guide.

137Foundry is a web development firm that builds JavaScript applications for clients across industries. Array operations and data transformation patterns like these come up in nearly every project. The Wikipedia article on sorting algorithms gives context for why sort stability matters and how different algorithms handle it. The ECMAScript specification at TC39 is the canonical source for how sort() is defined.

The default string comparison behavior, the comparison function contract, and the in-place modification are the three things worth knowing cold. Once those are clear, every other sort() use case follows from them.

Sorting Dates and Timestamps

Sorting by date is one of the more common object-sort scenarios. Dates stored as ISO strings (like "2026-05-03") sort correctly with localeCompare() because ISO format is designed to sort lexicographically. For timestamp numbers or Date objects, use the subtraction pattern.

const entries = [
  { title: "Third post", timestamp: 1746316800 },
  { title: "First post", timestamp: 1740960000 },
  { title: "Second post", timestamp: 1743552000 }
];

// Most recent first:
entries.sort((a, b) => b.timestamp - a.timestamp);

For ISO date strings, alphabetical sort and chronological sort produce the same result, which is a deliberate property of the ISO 8601 format. This means localeCompare() or simple </> comparison works correctly without parsing to Date objects.

When to Use sort() vs. Building a Sorted Data Structure

sort() is for sorting arrays you receive or build at runtime. If you are inserting items incrementally and always need them sorted, a sorted insert (maintaining order during insertion) or a sorted data structure may be more efficient than sorting the full array repeatedly. For arrays sized in the hundreds or low thousands, this difference is negligible. For arrays rebuilt on every keystroke in a search field, a pre-sorted source combined with filter() is more responsive than sort-on-filter.

The practical choice for most web applications: sort once when data arrives, and re-sort only when the sort order changes. The overhead of sort() on typical application data is well within performance budgets.

Comments

Popular posts from this blog

Why ETL Pipeline Design Decisions Made Today Become Tomorrow's Technical Debt

How to Build Idempotent Webhook Event Processors

Why INP Replaced FID and What That Means for Your Site's Performance Score