.reduce() is the most general-purpose array method — .map() and .filter() can both be implemented in terms of it — but it's also the one people find least intuitive at first.
const total = [1, 2, 3, 4].reduce((accumulator, current) => accumulator + current, 0);
// starts with accumulator = 0, then: 0+1=1, 1+2=3, 3+3=6, 6+4=10
The second argument to .reduce() (the initial value) matters more than it looks:
[].reduce((acc, x) => acc + x); // TypeError: Reduce of empty array with no initial value
[].reduce((acc, x) => acc + x, 0); // 0 — safe, because an initial value was provided
Without an initial value, .reduce() uses the array's first element as the starting accumulator and begins iterating from the second — which throws on an empty array since there's nothing to start from. Always pass an initial value unless you specifically want that first-element-as-seed behavior and have already confirmed the array is non-empty.
.reduce() isn't limited to summing numbers — it can build any shape of result, which is why it shows up so often once you're comfortable with it:
const wordCounts = ["a", "b", "a", "c", "b", "a"].reduce((counts, word) => {
counts[word] = (counts[word] || 0) + 1;
return counts;
}, {});
// { a: 3, b: 2, c: 1 }
const grouped = [{ type: "fruit", name: "apple" }, { type: "veg", name: "carrot" }, { type: "fruit", name: "pear" }]
.reduce((groups, item) => {
(groups[item.type] ??= []).push(item);
return groups;
}, {});
// { fruit: [{...apple}, {...pear}], veg: [{...carrot}] }