FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·055·9 MIN READ

055: Array Methods III: Reduce, Validation, and Sorting

TOPICS COVERED: Array Methods III: Reduce, Validation, and Sorting

Learning outcomes

By the end of this lesson, you can:

  • trace an accumulator through reduce() and choose a correct initial value;
  • use some() and every() for readable yes/no questions;
  • sort numbers and strings with a valid comparator;
  • distinguish mutating sort() from copying toSorted();
  • prefer a clearer method or loop when reduce() would hide intent.

Retrieval warm-up

Before moving on, connect this lesson to the array methods from lesson 043:

  1. Which 043 method returns the first matching element?
  2. What does findIndex() return when nothing matches?
  3. Predict [2, 5, 8].filter(number => number > 4).map(number => number * 2).

Answers: find(), -1, and [10, 16].

These answers are useful context here. find() and findIndex() locate a result, while filter() and map() transform an array in stages. The methods in this lesson solve a different set of problems: combine values, answer predicates, and order values.

Vocabulary

  • Accumulator: The running combined value carried from one reduce() callback call to the next. - Source: MDN: reduce
  • Current value: The element supplied to the callback during the current pass. - Source: MDN: reduce
  • Initial value: An explicit starting accumulator; when it is omitted, element 0 is used instead. - Source: MDN: reduce
  • Short-circuit: some() and every() stop iterating as soon as their outcome is known. - Source: MDN: every
  • Comparator: A function such as (a, b) => number that defines sort order through the sign of its return value. - Source: MDN: sort comparator
  • In place: A mutation performed directly on the original array, as with sort(), reverse(), or splice(). - Source: MDN: sort
  • Copying method: A non-mutating variant that returns a fresh array, such as toSorted(), toReversed(), or with(). - Source: MDN: Array
  • Reduce (official): "Array.prototype.reduce() executes a reducer function on each element, resulting in a single output value." - Source: MDN: Array.prototype.reduce()
  • Comparator (official): "A comparator is a function that defines sort order by returning negative, zero, or positive." - Source: MDN: Array.prototype.sort()

Beginner mental model

People often reach for reduce() when they need one result from an array, but the callback can be difficult to visualize at first. Treat the accumulator as a running total board: choose its starting value, visit each item, calculate the board's next value, and return that value. When the iteration ends, reduce() gives you the final board. The result can technically be any type; in this first model, use it for the natural case of adding numbers.

js
const quantities = [2, 1, 3];
const totalItems = quantities.reduce(
  (runningTotal, quantity) => runningTotal + quantity,
  0,
);

Trace the callback one pass at a time:

StepAccumulatorCurrentReturned next accumulator
start0-0
1022
2213
3336

The callback must return the next accumulator. If it forgets to return, the next pass receives undefined, which commonly leads to an unexpected NaN. Also choose an initial value whose type matches the result you want: 0 for a sum, 1 for a product when that identity is appropriate, "" for text, or [] for an array. Omitting the initial value has two consequences worth remembering: an empty array causes a TypeError, and the first element is unexpectedly adopted as the accumulator rather than being processed by the callback.

There is no prize for turning every array operation into a reduction. some() directly asks, "Does at least one item match?" It returns as soon as the predicate is truthy. every() asks, "Do all items match?" It returns as soon as the predicate is falsy. Both methods can avoid unnecessary work. For an empty array, some() is false and every() is true: there is no matching item for some(), and there is no item that disproves the condition for every(). If your rule says an empty collection is invalid, check its length separately.

Sorting has a different job and a different risk. sort() changes the source array and returns that same array reference. Without a comparator, JavaScript compares string forms, so [2, 100, 15].sort() becomes [100, 15, 2], not numeric ascending order. For numbers, use (a, b) => a - b for ascending order and (a, b) => b - a for descending order.

Modern toSorted() accepts the same comparator but returns a new, shallow array. The original array keeps its order. It has been broadly available since 2023, and it is the better choice when another part of the program still relies on the source order. "Shallow" means the array container is copied, not that nested values are deeply cloned.

Worked beginner example: cart checks and totals

Here, each method has one job. reduce() calculates totals, some() checks for an expensive line, every() validates quantities, and toSorted() prepares an ordered view without changing the input.

js
const quantities = [3, 2, 1];
const lineTotals = [12, 32, 45];

const itemCount = quantities.reduce(
  (count, quantity) => count + quantity,
  0,
);

const subtotal = lineTotals.reduce(
  (total, lineTotal) => total + lineTotal,
  0,
);

const hasExpensiveLine = lineTotals.some((lineTotal) => lineTotal >= 40);
const quantitiesAreValid = quantities.every(
  (quantity) => Number.isInteger(quantity) && quantity > 0,
);

const lowestLineFirst = lineTotals.toSorted((a, b) => a - b);

console.log(`Items: ${itemCount}`);
console.log(`Subtotal: $${subtotal}`);
console.log(`Expensive line: ${hasExpensiveLine}`);
console.log(`Valid quantities: ${quantitiesAreValid}`);
console.log(lowestLineFirst);
console.log(lineTotals);

Output:

text
Items: 6
Subtotal: $89
Expensive line: true
Valid quantities: true
[12, 32, 45]
[12, 32, 45]

The two number lists happen to look identical because lineTotals was already ascending. Reverse the source array if you want the preservation behavior to be visually obvious. The stronger test is reference identity: lowestLineFirst !== lineTotals is true. toSorted() created a new outer array and left the source array in its original order.

The subtotal has a simple trace: 0 + 12 = 12, then 12 + 32 = 44, then 44 + 45 = 89. The example uses prepared line totals so the lesson can focus on aggregation. In a later lesson about objects, each total can be associated with a named product.

Sorting precisely

The comparator does not return the item that should come first. It returns a number whose sign tells the sorting algorithm how the pair should be ordered:

  • negative: place a before b;
  • positive: place a after b;
  • zero or NaN: treat their order as equal.

A comparator should be pure and consistent. Do not write (a, b) => a > b. That expression returns only booleans, which become 0 or 1; it does not provide the expected negative/positive symmetry. For strings, use a.localeCompare(b), especially when real user text can contain accents. The comparator should describe ordering, not modify the values or depend on changing outside state.

js
const original = [30, 4, 100];
const sameArray = original.sort((a, b) => a - b);
console.log(original);              // [4, 30, 100]
console.log(sameArray === original); // true

const prices = [30, 4, 100];
const copied = prices.toSorted((a, b) => a - b);
console.log(prices);                // [30, 4, 100]
console.log(copied);                // [4, 30, 100]

This example isolates the mutation decision. After sort(), both original and sameArray refer to the reordered array. After toSorted(), prices remains unchanged and copied contains the new order. When debugging an unexpected reorder, inspect not only the array value but also which references point to it.

Intermediate example: checkout summary

A checkout summary needs several different operations: collect validation messages, add money, add units, and make a yes/no decision. Keep those operations matched to their purposes instead of hiding all four behaviors inside one large reducer.

js
function summarizeCart(quantities, lineTotals) {
  const errors = quantities
    .filter((quantity) => !Number.isInteger(quantity) || quantity < 1)
    .map((quantity) => `Invalid quantity: ${quantity}`);

  const subtotal = lineTotals.reduce((total, lineTotal) => total + lineTotal, 0);
  const itemCount = quantities.reduce((count, quantity) => count + quantity, 0);
  const canCheckout = quantities.length > 0 && errors.length === 0;

  return [subtotal, itemCount, canCheckout, errors];
}

console.log(summarizeCart(quantities, lineTotals));
// [89, 6, true, []]

The returned positions are documented here as subtotal, item count, checkout permission, and errors. Named object fields would become clearer once objects are taught, but an array is sufficient for this lesson. The quantities.length > 0 condition is deliberate: errors.length === 0 alone would also accept an empty array, just as every() does.

Why not force all four results through one reducer? A single callback would combine validation, arithmetic, and collection into code that beginners must mentally simulate. Separate passes make each invariant visible and are usually preferable for normal cart sizes. If performance later becomes a real concern, measure the actual workload before trading away clarity.

Optional advanced extension

Object comparators are only an optional preview. After object properties are introduced, a comparator can sort by stock and then use the product name as a tie-breaker:

js
const previewCart = [
  { name: "Notebook", stock: 3 },
  { name: "Pen", stock: 3 },
];
const inventoryOrder = previewCart.toSorted(
  (a, b) => a.stock - b.stock || a.name.localeCompare(b.name),
);

The record shape and property access are shown only to preview later lessons; they are not required today. The || expression evaluates the name comparison only when the stock comparison returns 0. ECMAScript sorting is stable, so comparator-equal items retain their earlier relative order. Even so, an explicit second key communicates the order the application intends and avoids leaving ties ambiguous to a reader.

Common mistakes and debugging

  • No initial value: [].reduce(callback) throws. Add an initial value with the correct type for the result.
  • No callback return: The next accumulator becomes undefined, often producing NaN on a later numeric operation.
  • Wrong initial type: Adding numbers to "0" concatenates strings. Start numeric totals at 0.
  • Reducing a yes/no question: Use some() or every() so evaluation can short-circuit and the intent is visible.
  • Assuming every() rejects empty arrays: [].every(...) is true. When emptiness is invalid, require values.length > 0 separately.
  • Default numeric sorting: Provide a numeric comparator instead of relying on string comparison.
  • Unexpected source reorder: Search for .sort( and use toSorted() when the requirement is to produce a copy.
  • Invalid boolean comparator: Return numeric differences or localeCompare() results.

When a reducer behaves unexpectedly, temporarily expand the callback and log the two values that matter on each pass:

js
const subtotal = lineTotals.reduce((total, lineTotal) => {
  console.log(total, lineTotal);
  return total + lineTotal;
}, 0);

The log should show the previous accumulator followed by the current line total. If the first accumulator is a string, if a pass shows undefined, or if a value is skipped, the trace points directly to the faulty initial value, missing return, or callback logic. This is often faster than trying to reason from the final NaN alone.

Best practices

  • Give accumulators semantic names such as total or count.
  • Provide an explicit initial value.
  • Use reduce() for genuine aggregation, not to prove cleverness.
  • Use some() and every() for predicate questions, and filter() for collecting failures.
  • Prefer toSorted() when original order is meaningful; document intentional in-place sort().
  • Keep comparators pure, numeric, and consistent.

Checkpoint

Trace a three-number reduction on paper using columns for accumulator, current value, and returned value. Then examine quantities.reduce((valid, quantity) => valid && quantity > 0, true) and rewrite it as the clearer quantities.every(...). Finish by comparing const sorted = values.sort(...) with const sorted = values.toSorted(...): predict both reference equality and source order before running the code. The explanation matters more than producing syntax quickly; being able to explain the state transition is what makes reducer and mutation bugs diagnosable.

Exercises

Core

From quantities, calculate total units and determine whether any quantity is 3 or more.

js
const units = quantities.reduce((total, quantity) => total + quantity, 0);
const hasBulkLine = quantities.some((quantity) => quantity >= 3);
console.log(units, hasBulkLine);

Output: 6 true

The reduction starts at numeric 0, and some() stops as soon as it finds a quantity meeting the threshold. With the lesson's quantities, the first value already satisfies the predicate.

Practice

Return lineTotals ordered from highest to lowest without changing the source.

js
const originalFirst = lineTotals[0];
const descendingTotals = lineTotals.toSorted((a, b) => b - a);

console.log(descendingTotals);
console.log(lineTotals[0] === originalFirst);

Output:

text
[45, 32, 12]
true

toSorted() supplies the copy, and reversing the comparator operands changes ascending numeric order to descending numeric order. The second log checks that the source's first element was not replaced.

Professional Extension

Write validateQuantities(quantities) returning an array whose first value is a Boolean validity result and whose second value is an array of messages for every invalid quantity. Validity must be false for an empty array.

js
function validateQuantities(quantities) {
  const errors = quantities
    .filter((quantity) => !Number.isInteger(quantity) || quantity < 1)
    .map((quantity) => `${quantity} is an invalid quantity`);

  return [quantities.length > 0 && errors.length === 0, errors];
}

console.log(validateQuantities([]));
console.log(validateQuantities([2, 0, 1.5]));

Output:

text
[false, []]
[false, ["0 is an invalid quantity", "1.5 is an invalid quantity"]]

The filter() pass preserves every invalid value so the caller receives every message, rather than only the first failure. The explicit length check handles the empty-array rule; errors.length === 0 handles the validity of nonempty input.

Recap

Describe the four reducer values on its first call when an initial value exists. Why does some() fit "at least one"? Why can every() be true for an empty array? What exactly does sort() mutate, and what kind of copy does toSorted() create?

Official references

Reader page: /javascript/lesson/055/array-methods-iii-reduce-validation-and-sorting