Skip to content
JavaScript js arrays 5 min read

Array.reduce()

Array.prototype.reduce() is the Swiss Army knife of array methods. While map and filter always hand you back another array, reduce collapses an entire array into a single value — and that value can be anything: a number, a string, an object, or even another array. Once you understand the accumulator pattern, you can express sums, groupings, lookups, and flattening with one expressive call. It has a reputation for being hard, but the mental model is small.

How reduce works

reduce walks the array left to right, carrying a running result called the accumulator. On each step your callback receives the accumulator and the current element, and whatever it returns becomes the accumulator for the next step. When the array is exhausted, the final accumulator is the result.

The full callback signature is (accumulator, currentValue, currentIndex, array), and reduce itself takes an optional second argument: the initial value.

const numbers = [1, 2, 3, 4];

const sum = numbers.reduce((acc, current) => acc + current, 0);

console.log(sum);

Output:

10

Here 0 is the initial value. On the first iteration acc is 0 and current is 1, returning 1; next acc is 1 and current is 2, and so on until 10.

Why the initial value matters

The second argument to reduce is optional, but omitting it changes the behaviour in two important ways. With an initial value, iteration starts at index 0 and the accumulator begins as whatever you passed. Without one, reduce uses the first element as the seed and starts iterating from index 1.

That difference is the source of one of the most infamous JavaScript runtime errors.

Gotcha: Calling reduce with no initial value on an empty array throws TypeError: Reduce of empty array with no initial value. Always pass an initial value unless you can guarantee the array is non-empty — and even then, it makes intent and the accumulator’s type explicit.

[].reduce((acc, x) => acc + x);        // TypeError
[].reduce((acc, x) => acc + x, 0);     // 0 — safe

The initial value also pins the accumulator’s type. If you want to build an object or array, seed it accordingly ({} or []).

Classic use cases

Sum and product

The canonical example — fold numbers into one total.

const prices = [9.99, 19.99, 4.5];

const total = prices.reduce((acc, price) => acc + price, 0);
console.log(total.toFixed(2));

Output:

34.48

Max (and min)

reduce can track the largest value seen so far. (For plain number arrays Math.max(...arr) is simpler, but reduce shines when comparing objects.)

const people = [
  { name: "Ada", age: 36 },
  { name: "Linus", age: 54 },
  { name: "Grace", age: 41 },
];

const oldest = people.reduce((max, p) => (p.age > max.age ? p : max));
console.log(oldest.name);

Output:

Linus

Group by

Building an object keyed by some property is one of reduce’s most useful jobs. Seed with {}, and push each item into the right bucket.

const orders = [
  { id: 1, status: "paid" },
  { id: 2, status: "pending" },
  { id: 3, status: "paid" },
];

const byStatus = orders.reduce((groups, order) => {
  (groups[order.status] ??= []).push(order);
  return groups;
}, {});

console.log(byStatus);

Output:

{
  paid: [ { id: 1, status: 'paid' }, { id: 3, status: 'paid' } ],
  pending: [ { id: 2, status: 'pending' } ]
}

The ??= (logical nullish assignment) lazily initialises each bucket to [] the first time a status appears.

Count occurrences

A specialised group-by that builds a frequency map.

const votes = ["yes", "no", "yes", "yes", "no"];

const tally = votes.reduce((counts, vote) => {
  counts[vote] = (counts[vote] ?? 0) + 1;
  return counts;
}, {});

console.log(tally);

Output:

{ yes: 3, no: 2 }

Flatten

You can concatenate nested arrays into one. (Modern code should prefer the dedicated flat method, but the reduce version shows the pattern.)

const nested = [[1, 2], [3, 4], [5]];

const flat = nested.reduce((acc, inner) => acc.concat(inner), []);
console.log(flat);

Output:

[ 1, 2, 3, 4, 5 ]

reduce vs alternatives

reduce is powerful but not always the clearest tool. Reach for a purpose-built method when one exists.

GoalBetter choiceUse reduce when…
Transform each itemmapnever — map is clearer
Keep matching itemsfilternever — filter is clearer
Sum / aggregate to one valuereducealways a good fit
Flatten one levelflat()you also transform during the fold (use flatMap)
Build an object from entriesObject.fromEntriesthe keys/values need custom logic

Readability tips

reduce becomes hard to read when the callback grows large or mutates the accumulator in surprising ways. A few habits keep it friendly:

  • Always return the accumulator — forgetting return yields undefined on the next pass.
  • Name the accumulator for what it is (total, groups, counts) rather than the generic acc.
  • If the callback exceeds a handful of lines, extract it into a named function.
  • Don’t force reduce where a simple for...of loop reads more plainly.
// Clear: named accumulator + extracted reducer
const addToCart = (cart, item) => {
  cart.total += item.price;
  cart.items.push(item.name);
  return cart;
};

const cart = [
  { name: "Pen", price: 2 },
  { name: "Notebook", price: 5 },
].reduce(addToCart, { total: 0, items: [] });

console.log(cart);

Output:

{ total: 7, items: [ 'Pen', 'Notebook' ] }

Best practices

  • Always supply an initial value unless you have a deliberate reason not to — it prevents empty-array crashes and fixes the accumulator’s type.
  • Never forget to return the accumulator from the callback.
  • Prefer map/filter/flat/some when they express the intent more directly; save reduce for genuine aggregations.
  • Avoid mutating arrays outside the reduce; build the result purely from the accumulator.
  • Use ??= or ?? to lazily initialise buckets and counts in group-by patterns.
  • Name your accumulator meaningfully so the next reader understands the shape being built.
Last updated June 1, 2026
Was this helpful?