Knowledge Base/guides/How to Use reduce in JavaScript Without Getting Confused

How to Use reduce in JavaScript Without Getting Confused

JavaScript reduce is confusing when you treat it like a magic array method. It gets much easier when you treat the accumulator as "the answer so far." The callback does one small job: take the answer so far, combine it with the current item, and return the next answer so far.

That sounds almost too plain, which is usually where the useful stuff hides.

Table of contents

The reduce shape

The basic shape is:

const result = array.reduce((accumulator, item) => {
  return nextAccumulator;
}, initialValue);

For a total, the accumulator starts as 0, then each item adds something to it:

const prices = [12, 8, 20];

const total = prices.reduce((totalSoFar, price) => {
  return totalSoFar + price;
}, 0);

console.log(total); // 40

Read that callback as a sentence:

Given the total so far and the current price, return the next total.

That is the whole contract. reduce calls the callback once for each item. Whatever you return becomes the accumulator for the next item.

The accumulator is the answer so far

The accumulator is not always a number. It is the unfinished version of the final answer.

If the final answer is a number, the accumulator is a number:

const totalMinutes = [10, 15, 5].reduce((total, minutes) => {
  return total + minutes;
}, 0);

console.log(totalMinutes); // 30

If the final answer is an object, the accumulator is an object:

const tickets = [
  { status: "open" },
  { status: "closed" },
  { status: "open" },
];

const counts = tickets.reduce((countsByStatus, ticket) => {
  countsByStatus[ticket.status] = (countsByStatus[ticket.status] ?? 0) + 1;
  return countsByStatus;
}, {});

console.log(counts); // { open: 2, closed: 1 }

If the final answer is one selected item, the accumulator can be that item:

const users = [
  { name: "Ada", score: 91 },
  { name: "Grace", score: 97 },
  { name: "Katherine", score: 94 },
];

const topUser = users.reduce((best, user) => {
  return user.score > best.score ? user : best;
}, users[0]);

console.log(topUser); // { name: "Grace", score: 97 }

That last example has a catch: it assumes the array has at least one item. If empty arrays are possible, handle that case before reducing:

function findTopUser(users) {
  if (users.length === 0) return null;

  return users.reduce((best, user) => {
    return user.score > best.score ? user : best;
  }, users[0]);
}

The accumulator should match the shape of the answer you are building. When it does not, the callback starts to feel slippery.

Trace reduce before trusting it

The fastest way to understand reduce is to trace it like a table.

const changes = [3, -1, 4];

const finalTotal = changes.reduce((total, change) => {
  return total + change;
}, 10);

console.log(finalTotal); // 16

Here is the accumulator path:

Steptotal before callbackchangeReturn value
Initial value10
110313
213-112
312416

The return value from step 1 becomes total in step 2. The return value from step 2 becomes total in step 3. The final return value becomes the value of finalTotal.

If that table does not make sense for your reducer, the code probably needs clearer names, smaller steps, or a plain loop.

Always pass an initial value

Without an initial value, JavaScript uses the first array item as the accumulator and starts the callback at the second item.

const numbers = [2, 3, 4];

const product = numbers.reduce((total, number) => {
  return total * number;
});

console.log(product); // 24

That might look fine until the array is empty:

const empty = [];

empty.reduce((total, number) => total + number);
// TypeError: Reduce of empty array with no initial value

The safer version says what the accumulator starts as:

const total = empty.reduce((total, number) => {
  return total + number;
}, 0);

console.log(total); // 0

The initial value should be the answer you want for an empty list:

Final answer typeCommon initial value
Sum0
Product1
Joined string""
Built array[]
Lookup object{}
Map lookupnew Map()
Nullable selected itemnull

That rule prevents most reduce bugs before they get a chance to be dramatic.

Name the accumulator by its job

acc is short, but it hides meaning. That is fine in a tiny example. In real code, use names that describe what is being accumulated.

Harder to read:

const result = orders.reduce((acc, order) => {
  acc += order.quantity * order.price;
  return acc;
}, 0);

Easier to read:

const revenue = orders.reduce((totalRevenue, order) => {
  return totalRevenue + order.quantity * order.price;
}, 0);

For object accumulators, the name matters even more:

const usersById = users.reduce((lookup, user) => {
  lookup[user.id] = user;
  return lookup;
}, {});

lookup is not perfect, but it is better than acc. usersById would be even clearer inside a longer reducer:

const usersById = users.reduce((usersById, user) => {
  usersById[user.id] = user;
  return usersById;
}, {});

The repetition is not a problem. It makes the contract obvious: this reducer is building usersById.

Use reduce only when the output is one result

Use reduce when an array becomes one final value:

const completedCount = tasks.reduce((count, task) => {
  return task.done ? count + 1 : count;
}, 0);

That one result can be a number, object, array, string, Map, or selected item. The key is that the whole array collapses into one answer.

If every item becomes another item, use map:

const taskTitles = tasks.map((task) => task.title);

If some items survive, use filter:

const completedTasks = tasks.filter((task) => task.done);

If you only need the first match, use find:

const firstBlockedTask = tasks.find((task) => task.blocked);

The JavaScript Array Methods guide covers those choices as a group. The short version is: choose the method by the shape of the output, not by how clever the callback looks.

Common reduce mistakes

Forgetting to return the accumulator

This reducer looks like it updates the total, but it returns undefined after the first callback:

const total = [1, 2, 3].reduce((sum, number) => {
  sum + number;
}, 0);

console.log(total); // undefined

The fixed version returns the next accumulator:

const total = [1, 2, 3].reduce((sum, number) => {
  return sum + number;
}, 0);

With an expression arrow function, the expression is returned automatically:

const total = [1, 2, 3].reduce((sum, number) => sum + number, 0);

Changing the input items by accident

Mutating the accumulator can be fine when the accumulator is a new object created for the reduction:

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

Mutating the original items is a different story:

const normalized = users.reduce((list, user) => {
  user.name = user.name.trim();
  list.push(user);
  return list;
}, []);

That changes the objects inside users. If the caller expects the original data to survive untouched, create new objects:

const normalized = users.reduce((list, user) => {
  list.push({
    ...user,
    name: user.name.trim(),
  });

  return list;
}, []);

For object copying details, see The Spread Operator.

Using reduce for a hard-to-read chain

This works, but it makes the reader earn every inch:

const labels = users.reduce((list, user) => {
  if (!user.active) return list;
  list.push(`${user.id}:${user.name.trim().toLowerCase()}`);
  return list;
}, []);

This version says the same thing in clearer stages:

const labels = users
  .filter((user) => user.active)
  .map((user) => `${user.id}:${user.name.trim().toLowerCase()}`);

reduce can imitate map and filter. That does not mean it should. Code is read more often than it is admired.

When a loop is clearer

Use a for...of loop when you need early exit, multiple branches, sequential await, or several pieces of state that change in ways a reducer would hide.

let matchingUser = null;

for (const user of users) {
  if (!user.active) continue;

  if (user.email === targetEmail) {
    matchingUser = user;
    break;
  }
}

You could force that into reduce, but the loop tells the truth: scan active users, stop when you find the match.

The for...of loop is not a beginner fallback. It is the right tool when control flow matters.

Key Takeaways

  • reduce carries one accumulator through the array and returns the final accumulator.
  • The accumulator should match the shape of the answer you are building.
  • Always pass an initial value so empty arrays behave predictably.
  • Name the accumulator by its job: totalRevenue, countsByStatus, usersById, not always acc.
  • Use reduce for one accumulated result. Use map, filter, find, or for...of when those shapes are clearer.
Share this article:
javascriptarraysfunctionalfundamentals