055: 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()andevery()for readable yes/no questions; - sort numbers and strings with a valid comparator;
- distinguish mutating
sort()from copyingtoSorted(); - 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:
- Which 043 method returns the first matching element?
- What does
findIndex()return when nothing matches? - 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()andevery()stop iterating as soon as their outcome is known. - Source: MDN: every - Comparator: A function such as
(a, b) => numberthat 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(), orsplice(). - Source: MDN: sort - Copying method: A non-mutating variant that returns a fresh array, such as
toSorted(),toReversed(), orwith(). - 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.
const quantities = [2, 1, 3];
const totalItems = quantities.reduce(
(runningTotal, quantity) => runningTotal + quantity,
0,
);
Trace the callback one pass at a time:
| Step | Accumulator | Current | Returned next accumulator |
|---|---|---|---|
| start | 0 | - | 0 |
| 1 | 0 | 2 | 2 |
| 2 | 2 | 1 | 3 |
| 3 | 3 | 3 | 6 |
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.
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:
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
abeforeb; - positive: place
aafterb; - 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.
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.
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:
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 producingNaNon a later numeric operation. - Wrong initial type: Adding numbers to
"0"concatenates strings. Start numeric totals at0. - Reducing a yes/no question: Use
some()orevery()so evaluation can short-circuit and the intent is visible. - Assuming
every()rejects empty arrays:[].every(...)istrue. When emptiness is invalid, requirevalues.length > 0separately. - Default numeric sorting: Provide a numeric comparator instead of relying on string comparison.
- Unexpected source reorder: Search for
.sort(and usetoSorted()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:
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
totalorcount. - Provide an explicit initial value.
- Use
reduce()for genuine aggregation, not to prove cleverness. - Use
some()andevery()for predicate questions, andfilter()for collecting failures. - Prefer
toSorted()when original order is meaningful; document intentional in-placesort(). - 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.
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.
const originalFirst = lineTotals[0];
const descendingTotals = lineTotals.toSorted((a, b) => b - a);
console.log(descendingTotals);
console.log(lineTotals[0] === originalFirst);
Output:
[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.
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:
[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?
