TechEarl

JavaScript Array.reduce, Explained: Sum, Build Objects, and When Not to Use It

How Array.reduce really works: the accumulator and current value, summing numbers, the empty-array TypeError that bites everyone, building objects and Maps, and when Object.groupBy or map/filter is the better tool.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
How JavaScript Array.reduce works: sum numbers, build objects and Maps, the empty-array TypeError, and when to reach for map, filter, or Object.groupBy instead.

Array.prototype.reduce walks an array left to right, carrying a running value (the accumulator) from one element to the next, and returns that single final value. The one-line summary I give people: it folds a list of things down into one thing, and the thing it folds into can be a number, a string, an object, a Map, or another array.

javascript
const nums = [3, 1, 4, 1, 5];
const sum = nums.reduce((acc, current) => acc + current, 0);
// 14

The callback gets (accumulator, current) on each step. Whatever you return becomes the accumulator for the next element. The 0 after the callback is the initial value, the seed the accumulator starts from. That seed is the part most reduce bugs come down to, so I am going to lead with it before anything else.

Always pass the initial value

This is the single most-hit reduce bug, and it is worth burning into muscle memory: if you call reduce with no initial value on an empty array, it throws.

javascript
[].reduce((a, b) => a + b);
// Uncaught TypeError: Reduce of empty array with no initial value

When you omit the seed, reduce uses the array's first element as the starting accumulator and begins iterating from index 1. On an empty array there is no first element to grab, so it cannot start, and it throws a TypeError rather than guessing. Pass the seed and the empty case just returns the seed:

javascript
[].reduce((a, b) => a + b, 0);
// 0, no throw

The seed is technically optional, but the only time omitting it is safe is when you can guarantee the array is never empty, and in real code you usually cannot make that promise (the data came from a fetch, a filter, a user). There is a second, quieter reason to always pass it: the seed pins the type of the accumulator. Sum into 0 and the accumulator is a number from step one. Sum into "" by accident and you are doing string concatenation. Build into {} or new Map() and the accumulator is the right shape on the first iteration instead of being whatever your first element happened to be. My rule is simple: every reduce gets an explicit second argument, no exceptions.

Summing numbers

The canonical example. No, there is no Math.sum for arrays, so reduce is the idiomatic one-liner:

javascript
const prices = [19.99, 4.5, 12];
const total = prices.reduce((acc, price) => acc + price, 0);
// 36.49

If money is involved, remember these are IEEE-754 floats: 0.1 + 0.2 is not 0.3, and that error rides along through a reduce. Sum integer cents and divide at the end, or reach for a decimal library, the same as you would anywhere else in JavaScript. That is not a reduce problem, but reduce is where people first notice it.

Building an object or a Map

Reduce earns its keep when the result is not a scalar. A common shape is turning an array of records into a lookup keyed by id:

javascript
const users = [
  { id: 7, name: "Ada" },
  { id: 12, name: "Linus" },
];

const byId = users.reduce((acc, user) => {
  acc[user.id] = user;
  return acc;
}, {});
// { 7: {id:7,name:"Ada"}, 12: {id:12,name:"Linus"} }

The thing people forget here is the return acc. The callback has to hand the accumulator back every step. Mutate it, then return it. If you would rather not mutate, return a fresh object with spread ({ ...acc, [user.id]: user }), but that allocates a new object per element and is genuinely slower on large arrays, so for a plain build-up I mutate the accumulator and move on.

A Map is often the better target than a plain object: it keeps insertion order, takes any key type (not just strings), and has no prototype keys to collide with.

javascript
const byIdMap = users.reduce(
  (acc, user) => acc.set(user.id, user),
  new Map()
);

Map.prototype.set returns the Map, so the callback body collapses to a single expression with no separate return line.

Where Object.groupBy now replaces the reduce-to-grouped-object pattern

For years the "group these items by a key" task was the textbook reduce, and you still see it everywhere:

javascript
const products = [
  { name: "cap", type: "hat" },
  { name: "beanie", type: "hat" },
  { name: "tee", type: "shirt" },
];

// The old reduce-to-grouped-object pattern:
const grouped = products.reduce((acc, p) => {
  (acc[p.type] ??= []).push(p);
  return acc;
}, {});

As of Baseline 2024 you do not need reduce for this at all. Object.groupBy does exactly this, and reads as what it does:

javascript
const grouped = Object.groupBy(products, (p) => p.type);
// { hat: [ {cap}, {beanie} ], shirt: [ {tee} ] }

It is supported across current browsers and in Node 21+ (it landed as a static method after an earlier Array.prototype.group proposal was renamed over web-compatibility issues, which is also a fine reason not to monkey-patch Array.prototype yourself). If you need a real Map instead of a null-prototype object (so non-string keys work), Map.groupBy is the sibling. For the wider tour of which method does what, see my JavaScript array methods guide. When grouping is all you are doing, group with the grouping tool.

Reach for map, filter, or groupBy first; reduce when you are genuinely folding

reduce is the most general array method, which is exactly why it is overused. Almost anything can be expressed as a reduce, but "can be" is not "should be." A reduce that only transforms each element is a map wearing a disguise; one that only keeps some elements is a filter; one that builds a grouped object is Object.groupBy. Each of those names tells the next reader what is happening at a glance, where a reduce makes them parse the callback to find out.

My heuristic:

  • Transforming every element one-to-one, reach for map.
  • Keeping a subset, reach for filter.
  • Grouping by a key, reach for Object.groupBy (or Map.groupBy).
  • Flattening one level, reach for flat (or flatMap to map-and-flatten in one pass).
  • Collapsing the whole array into a single value whose computation needs the running result, that is a genuine fold, and that is reduce's job: a sum, a product, a min/max, a running balance, threading a value through a pipeline of functions.

That last category is where reduce is the clear, idiomatic choice and nothing else fits as cleanly. Everywhere else, the named method is easier to read and usually no slower. There is a small te-prefixed helper I keep around for the one fold I write most often, totalling a numeric field:

javascript
const teSumBy = (arr, key) => arr.reduce((acc, item) => acc + item[key], 0);

teSumBy([{ qty: 2 }, { qty: 5 }], "qty"); // 7

It is a real fold (a running total that needs the accumulator), it seeds with 0, and it is named for what it does. That is the bar for reaching for reduce.

See also

Sources

Authoritative references this article was fact-checked against.

Tagsjavascriptarray reducereduceArray.prototype.reduceObject.groupBysum arrayaccumulator

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

JavaScript Promises Explained: A Complete Guide

How JavaScript promises actually work: the three states, .then/.catch/.finally, async/await as the default modern style, and the gotchas that bite (the explicit-construction antipattern and the await-in-a-loop serialization trap).