Module: JavaScript
JavaScript·055·6 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

  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].

Vocabulary

  • Accumulator: Running combined value carried through reduce calls. — Source: MDN: reduce
  • Current value: Element supplied to the callback on this pass. — Source: MDN: reduce
  • Initial value: Explicit starting accumulator; omitting it uses element 0. — Source: MDN: reduce
  • Short-circuit: some/every stop iterating once the outcome is decided. — Source: MDN: every
  • Comparator: (a,b)=>number defining sort order by sign. — Source: MDN: sort comparator
  • In place: Mutation performed directly on the original array (sort/reverse/splice). — Source: MDN: sort
  • Copying method: Non-mutating variant returning a fresh array (toSorted/toReversed/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

reduce() is a running total board. Start with an explicit value, visit each item, return the board's next value, and finally receive the board. Although the final result may be any type, today use it where aggregation is natural: a number.

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

Trace it:

StepAccumulatorCurrentReturned next accumulator
start0-0
1022
2213
3336

The callback must return the next accumulator. Supply an initial value matching the result: 0 for a sum, 1 for a product when appropriate, "" for text, or [] for an array. Without one, an empty array throws TypeError, and the first element unexpectedly becomes the accumulator.

Do not make every operation a reduction. some() directly asks "does at least one match?" and returns early on the first truthy predicate. every() asks "do all match?" and returns early on the first falsy predicate. For an empty array, some() is false and every() is true; no item disproves the every-condition.

Sorting is different. sort() changes the source array and returns that same reference. Without a comparator it compares string forms, so [2, 100, 15].sort() becomes [100, 15, 2]. Numeric ascending uses (a, b) => a - b; descending uses (a, b) => b - a.

Modern toSorted() accepts the same comparator but returns a new, shallow array and leaves the original order alone. It has been broadly available since 2023. Use it when preserving source order matters.

Worked beginner example: cart checks and totals

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 match because the source was already ascending. Reverse the source to make preservation visible. More importantly, lowestLineFirst !== lineTotals: toSorted() created a new outer array and did not reorder the source.

The subtotal trace is 0 + 12 = 12, then 12 + 32 = 44, then 44 + 45 = 89. Prepared line totals keep this lesson focused on aggregation; later object lessons can associate each total with a named product.

Sorting precisely

Comparator results mean:

  • 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 returns only booleans (0 or 1) and violates the expected negative/positive symmetry. For strings, use a.localeCompare(b), particularly when real user text may contain accents.

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]

Intermediate example: checkout summary

Keep each operation matched to its purpose instead of building one giant 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 become clearer after objects are taught, but are not required for this lesson. Why not force all four results through one reducer? It combines validation, arithmetic, and collection into a callback beginners must mentally simulate. Clear passes are usually preferable for normal cart sizes.

Optional advanced extension

Object comparators are an optional preview only. After object properties are introduced, sorting by stock and then name can look like this:

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),
);

This small record shape and its property access are shown only to preview later lessons and are not required today. The || evaluates the name comparison only when stock comparison returns 0. ECMAScript sorting is stable, meaning comparator-equal items retain their earlier relative order, but an explicit second key communicates the intended order.

Common mistakes and debugging

  • No initial value: [].reduce(callback) throws. Add the correctly typed initial value.
  • No callback return: the next accumulator becomes undefined, often producing NaN.
  • Wrong initial type: "0" plus numbers concatenates strings. Start numeric totals at 0.
  • Reducing a yes/no question: use some() or every() so evaluation can short-circuit.
  • Assuming every() rejects empty arrays: [].every(...) is true; separately require values.length > 0 when emptiness is invalid.
  • Default numeric sorting: provide a comparator.
  • Unexpected source reorder: search for .sort( and replace with toSorted() when a copy is intended.
  • Invalid boolean comparator: return numeric differences or localeCompare() results.

Debug a reducer by temporarily expanding it:

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

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()/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. Next, 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. The explanation matters more than quickly producing syntax.

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

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

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"]]

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