JAVASCRIPT / ARRAYS
reduce for folding arrays into one value
Collapse any array into one value with reduce: pick the right initial value, return the accumulator every step, and know when iteration starts at index 1.
What you will learn
- Fold an array to one value by returning the next accumulator from the callback
- Use an initial value to fix the result type and keep empty arrays from throwing
- Predict which index the callback starts at with and without a seed
- Fold into objects, strings, or Maps, not only numbers
Understanding reduce for folding arrays into one value
reduce is the array method for turning many values into one. You hand it a callback that receives the accumulator so far and the current element, and whatever that callback returns becomes the accumulator passed to the next element; the accumulator left after the last element is reduce's return value. The mental model is a for loop with a running variable declared outside it, except the running variable is threaded through return values instead of reassigned. That is exactly why a missing return silently poisons the entire fold rather than causing a syntax error.
The second argument, the initial value, does more than save a line. It fixes what the accumulator starts as and therefore what type the result has, and it decides where iteration begins: with a seed the callback runs once per element starting at index 0, without one element 0 is quietly borrowed as the seed and the callback runs length - 1 times starting at index 1. That borrowing is also why reduce on an empty array with no seed throws a TypeError instead of returning 0 or undefined; there is no element to borrow and the method refuses to invent a starting value it cannot know.
Nothing requires the accumulator to resemble the elements. Seed with {} and you fold records into a lookup table, seed with '' and you fold into a string, seed with new Map() and you count occurrences. That generality is why reduce behaves like the primitive sitting underneath map, filter, and join, and also why it is easy to abuse: when one callback is filtering, transforming, and grouping at once, a plain loop or a filter/map chain communicates the intent better even though reduce can express it.
const prices = [4.5, 2.25, 10, 3.25];
const total = prices.reduce((sum, price) => sum + price, 0);
console.log('total:', total);
// The value the callback returns is the `sum` the next call receives.
prices.reduce((sum, price, i) => {
console.log(`i=${i} sum=${sum} -> ${sum + price}`);
return sum + price;
}, 0);
// No initial value: prices[0] becomes the seed, so the callback starts at i=1.
const noSeed = prices.reduce((sum, price, i) => {
if (i === 1) console.log('first call:', 'i=' + i, 'sum=' + sum);
return sum + price;
});
console.log('noSeed:', noSeed, noSeed === total);Whatever the callback returns becomes the accumulator for the next element, and the initial value decides both the result's type and whether iteration starts at index 0 or 1.
Worked examples
Folding strings into an object
Shows that the seed decides the result type, and that a mutating fold returns the very object you seeded.
const words = ['ox', 'cat', 'emu', 'bee', 'horse'];
const seed = {};
const byLength = words.reduce((groups, word) => {
(groups[word.length] ??= []).push(word);
return groups;
}, seed);
console.log(JSON.stringify(byLength));
console.log(byLength === seed);Example explained
Line 1The seed {} makes the accumulator an object, so folding an array of strings yields something that is not a string.
Line 2groups[word.length] ??= [] creates a bucket only the first time that length appears, then .push adds to the existing one.
Line 3return groups is mandatory: without it the second call receives undefined and reading a property off it throws.
Line 4byLength === seed is true because this fold mutates one object instead of building a new one per element.
The two ways reduce breaks
Demonstrates a callback that forgets to return and an unseeded reduce over an empty array.
const nums = [1, 2, 3];
console.log(nums.reduce((acc, n) => { acc + n; }, 0));
try {
[].reduce((a, b) => a + b);
} catch (err) {
console.log(err.name, '<- empty array, no seed');
}
console.log([].reduce((a, b) => a + b, 0));Example explained
Line 1The block body { acc + n; } computes a sum and discards it, so the arrow returns undefined and every later call is handed undefined.
Line 2An unseeded reduce on [] has no element to use as the starting accumulator, so it throws instead of guessing a zero value.
Line 3The same empty array with an explicit 0 returns 0 and never calls the callback at all.
Fold direction with reduceRight
Makes the sequential, left-to-right nature of reduce visible by nesting parentheses.
const parts = ['a', 'b', 'c'];
console.log(parts.reduce((acc, x) => `(${acc}${x})`));
console.log(parts.reduceRight((acc, x) => `(${acc}${x})`));
console.log(parts.reduce((acc, x) => `(${acc}${x})`, 'S'));Example explained
Line 1reduce folds left: 'a' is the seed, then 'b', then 'c', so the deepest parentheses hold the earliest elements.
Line 2reduceRight seeds with the last element and walks backwards, which flips the order inside each pair of parentheses.
Line 3Passing 'S' as the seed adds one extra callback call, producing three nesting levels instead of two.
Line 4For commutative steps like addition the direction is invisible; for concatenation or subtraction it changes the answer.
Important notes
reduce skips holes, so [1, , 3].reduce((a, b) => a + b, 0) is 4 and the callback runs twice; a sparse array is not the same as one filled with undefined.
Returning { ...acc, [key]: value } rebuilds the whole object on every element, turning a linear fold into quadratic work; mutate the accumulator you created instead.
Common mistakes
Writing a block-bodied callback with no return, like (acc, n) => { acc + n; }: undefined is fed forward and the whole reduce evaluates to undefined instead of a sum.
Returning acc.push(item): push returns the new length, so the next call gets a number and the fold dies with 'acc.push is not a function'.
Omitting the initial value on an array that can be empty, typically after a filter: it passes on sample data and throws 'Reduce of empty array with no initial value' on real input.
Try it yourself
Change, predict, then run
With const orders = [{ who: 'ana', cents: 500 }, { who: 'bo', cents: 250 }, { who: 'ana', cents: 125 }], use reduce seeded with {} to build { ana: 625, bo: 250 }. Then run the identical reduce on [] and confirm it returns {} rather than throwing.
Open the JavaScript workspaceCheck your understanding
How does [10, 20, 30].reduce((acc, n) => acc + n) differ from [10, 20, 30].reduce((acc, n) => acc + n, 0)?
- Both return 60, but the first calls the callback twice and throws if the array is empty
- The first returns 60 and the second returns 0, because the initial value replaces the first element
- The first returns NaN, because the accumulator starts out as undefined
- Both call the callback three times; the initial value only affects the result's type
Show answer
Without an initial value reduce uses element 0 as the starting accumulator and begins the callback at index 1, so it runs twice; with 0 as the seed it begins at index 0 and runs three times. The totals match for this array, but only the seeded call survives an empty array. The NaN option is tempting if you assume an unseeded accumulator defaults to undefined, and the last option is tempting because the results look identical, yet the number of callback calls really does differ.