reduce()
ES5+Executes a reducer function on each element of the array, resulting in a single output value.
Syntax
array.reduce(callback(accumulator, currentValue, index, array), initialValue)Parameters
callback Function A function to execute on each element
initialValue any optionalInitial value for the accumulator
Return Value
The single value that results from the reduction
Examples
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, cur) => acc + cur, 0);
console.log(sum); 📌 When to Use
Use reduce() when an array must collapse into something that is not a same-length array: a total, a maximum, a lookup object keyed by id, a Map of groups, a deduplicated list, or even a composed function. The mental test is simple - if the answer to "what comes out?" is one accumulated thing rather than one output per input, reduce() fits. Summing order totals, tallying vote counts, indexing records by id for O(1) lookup, and building nested structures from flat rows are all classic fits. That versatility is also its trap: reduce() can imitate map() and filter(), but doing so hides intent behind accumulator plumbing, so prefer the specialized methods when they express the job directly. For grouping specifically, modern runtimes offer Object.groupBy() and Map.groupBy() (ES2024), which state the intent more clearly than a hand-rolled reducer. Reach for reduce() over a chained map().filter() pipeline when profiling shows the intermediate arrays matter, or when the accumulation genuinely needs to see running state - such as computing a running balance where each line depends on everything processed before it.
⚠️ Common Mistakes
Omitting the initial value. Without it, the spec takes the first element as the starting accumulator and begins iterating at index 1 - which silently produces wrong results when elements are objects (there is nothing to sum onto), and throws a TypeError outright when the array is empty. Empty arrays are exactly what API responses look like on bad days, so this bug tends to surface in production.
Forgetting to return the accumulator from every code path of the callback. A branch that updates state but returns nothing hands undefined to the next iteration, which then explodes with "cannot read properties of undefined" one element later - a confusing off-by-one error message that points at the wrong line.
Reimplementing map() or filter() with reduce(). acc.concat(fn(x)) inside a reducer does the same work as map() but forces every reader to reverse-engineer the accumulator to discover it. Reserve reduce() for genuine aggregation; use the specialized methods when they say what you mean.
Spreading the accumulator on every iteration: (acc, x) => ({ ...acc, [x.id]: x }) copies the entire accumulated object each time, turning a linear job into O(n^2). With 10,000 items that is tens of millions of property copies. Mutate the accumulator you created yourself - (acc[x.id] = x, acc) - it is local to the reduction and perfectly safe.
Swapping the callback parameters. The reducer receives (accumulator, currentValue), not (value, accumulator); writing (cur, acc) => acc + cur.price still runs but accumulates garbage like "NaN" or "[object Object]undefined". Because nothing throws, the wrong output can travel far before anyone notices.
Using reduce() with an async callback and expecting sequential awaits. The accumulator becomes a Promise after the first iteration, so each step must await acc before doing anything - easy to get wrong and hard to read. A plain for...of loop with await, or Promise.all() when order does not matter, is clearer and less error-prone.
✅ Best Practices
Always pass an initial value, even when it feels redundant. It guarantees correct behavior on empty arrays, pins down the accumulator type for TypeScript inference, and means the callback runs for every element instead of skipping the first - three classes of bugs eliminated by one extra argument.
Give complex reducers a name and a home: orders.reduce(toRevenueByRegion, {}) tells the story at the call site, and the extracted function can be unit-tested with hand-crafted accumulator states - something nearly impossible to do cleanly with a sprawling inline closure.
Collapse a hot map().filter().map() chain into one reduce() pass when profiling shows the intermediate arrays hurt - one iteration, one allocation. Do this as an optimization with a comment explaining the original pipeline, not as a default style.
It is fine - and fast - to mutate an accumulator object or array that the reduction itself created, because nothing outside the reducer can observe it mid-flight. The purity rule that matters is never mutating the input array or captured outer state.
Prefer purpose-built alternatives when they exist: Object.groupBy() for grouping (ES2024), flat() for flattening, and Math.max(...arr) for small maxima all state intent more clearly than the equivalent reducer, leaving reduce() for the aggregations that genuinely need it.
⚡ Performance Notes
reduce() itself is a single O(n) pass with no intrinsic allocation - the cost profile is entirely determined by what the callback does. The notorious performance trap is an accumulator that gets copied every iteration: { ...acc } or [...acc, x] inside the reducer makes the whole reduction O(n^2), which is why a "clean-looking" immutable groupBy can take seconds on a 50,000-row dataset while the mutating version finishes in milliseconds. In V8, a monomorphic reducer callback (always receiving the same object shapes) inlines well and performs close to a hand-written for loop; a plain loop still wins slightly for trivial numeric sums because it avoids per-element function calls. Compared with chained array methods, reduce() saves one intermediate array per fused stage, which becomes measurable in the hundreds of thousands of elements. For readability-critical code at small scale, none of this matters - choose the clearest expression.
🌍 Real World Example
Grouping Data by Category
Grouping flat records into a keyed structure is probably the single most common real-world reduce(). The API returns transactions as a flat list, but the dashboard needs them bucketed by category to render one section per spending type. The reducer builds an object whose keys are category names and whose values are arrays of matching transactions, creating each bucket lazily the first time its category appears. Note that the accumulator object is mutated directly and returned - safe here because reduce() created it - and that {} as the initial value makes the code correct even for an empty transaction list.
const transactions = [
{id: 1, category: 'food', amount: 50},
{id: 2, category: 'transport', amount: 30},
{id: 3, category: 'food', amount: 25}
];
const byCategory = transactions.reduce((groups, tx) => {
const key = tx.category;
groups[key] = groups[key] || [];
groups[key].push(tx);
return groups;
}, {});
// Result: {food: [{...}, {...}], transport: [{...}]}