Iteration and array transformation · Lesson 34 of 77 · practice

reduce: many values into one

About 70 minutes.

By the end of this lesson you can

  • Explain what the accumulator is and how it travels from one call to the next.
  • Write a reduce that produces a number from an array of numbers.
  • Write a reduce that produces an object, and say why the accumulator must be returned.
  • Describe what changes when the initial value is left out, including the empty-array case.
  • Decide honestly when a reduce is clearer than a for...of loop and when it is not.

When the answer is not a list

Why it matters

map gives you a list. filter gives you a shorter list. But plenty of questions have a single answer: what is the total, what is the largest, how many of each kind are there, what does this whole array look like as one string. Those cannot be expressed as a list, and none of the methods so far produce them.

reduce is the general tool for collapsing many values into one. It is the most powerful method in the family and by some distance the hardest to read at first, which is why it comes last. Everything you can do with map or filter, you could do with reduce — that is not a recommendation, it is a warning about how easily it is overused.

The honest framing: learn reduce properly so you recognise it, use it where it is genuinely the clearest option, and reach for a plain for...of loop without embarrassment when it is not.

The running total on a receipt

An analogy

Think about totalling a receipt by hand. You start at zero. You read the first line and add it to your running total. You read the next line and add that. The running total is carried from one line to the next, and when the lines run out, the running total is the answer.

Two things are moving: the item you are looking at right now, and the running total you are carrying. That carried value is what JavaScript calls the accumulator, and it is the only genuinely new idea in this lesson. Everything else is arranging that idea into syntax.

The reducer's two arguments

Explanation

You write array.reduce(reducer, initialValue). The reducer receives the accumulator first and the current element second — accumulator, current, in that order, every time. What the reducer returns becomes the accumulator for the next call, and the final return value is what reduce hands back.

That last sentence is the whole mechanism, and it is worth reading twice. The reducer does not modify anything; it produces the next value of the carried result. On the first call the accumulator is the initial value you supplied. On the second call it is whatever the first call returned. And so on, until the elements run out.

Like the other methods, the reducer can also take the index and the array as third and fourth arguments. You will rarely want them, and the accumulator being first is the thing to fix in memory — swapping the first two parameters produces code that runs happily and gives nonsense.

The initial value is not really optional

Explanation

The second argument to reduce can be left out, and it changes the behaviour in two ways. Without it, the accumulator starts as the FIRST element of the array and iteration begins at the second, so your reducer is called one fewer time. With it, the accumulator starts as your value and iteration begins at the first element.

For summing numbers both forms give the same answer, so it is tempting to drop it. Do not. Calling reduce on an empty array with no initial value throws a TypeError — there is no first element to start from and nothing to return, so the method refuses rather than inventing an answer. An initial value of 0 makes the empty case return 0 without a special branch. (The exact wording of that error differs between JavaScript engines, so match on the error type rather than its message.)

There is a second reason, which matters more once your accumulator is not a number. The initial value is what declares the SHAPE of the result: {} for an object, [] for an array, 0 for a count, "" for a string. Someone reading reduce((counts, item) => ..., {}) knows what is being built before they read the reducer at all.

reduce can build any shape

Explanation

The accumulator does not have to be a number. Start with {} and you can build a tally: for each item, add one to the count stored under its category. Start with [] and you can build an array, though if the result is one output per input you wanted map. Start with "" and you can build a string, though join is usually better.

The tally is the case where reduce genuinely earns its place, because there is no other method that does it. The pattern is always the same: read the current value out of the accumulator, work out the new value, write it back, and return the accumulator.

That write-back mutates the accumulator object rather than creating a new one each time, which is a deliberate and normal choice here — the object was created by your initial value and nothing outside the reduce can see it until the reduce finishes. Creating a fresh copy on every element instead is possible and much slower, and the data-modelling module returns to that trade-off.

Forgetting to return the accumulator

A common mistake

This is the reduce bug, and everyone writes it. If your reducer has a braced body and does not return the accumulator, the next call receives undefined, and the final answer is undefined. Nothing throws; you just get nothing.

It bites hardest with the tally pattern, because the interesting line — counts[key] = counts[key] + 1 — looks like the whole job, and the return counts underneath it looks like ceremony. It is not ceremony. It is the only thing that carries your work forward.

The rule of thumb: every path through a reducer must end in a return, including any early exits inside an if. If you see undefined coming out of a reduce, look for the missing return before you look anywhere else.

reduce versus a plain loop, honestly

A mental model

reduce is worth reaching for when the operation genuinely is 'combine these into one value' and the reducer is a short expression — sums, products, maximums, tallies, flattening. In those cases it states the intent in one line and there is no loop machinery to misread.

A for...of loop is the better answer when the body is long, when several different things are being accumulated at once, when you need to stop early, or when the reducer would need conditionals inside conditionals. A reduce with a ten-line body and three ifs is not clever, it is a loop wearing a costume — and it is harder to step through in a debugger, because the body is a function call rather than a block.

Experienced developers disagree about where the line sits, and that is fine. What is not fine is choosing reduce because it looks advanced. Pick the version a tired colleague could read at five o'clock.

Recap

Recap

reduce carries an accumulator from one call to the next: the reducer takes (accumulator, current) and returns the next accumulator, and the last one returned is the answer. Always pass an initial value — it fixes the empty-array TypeError and it announces the shape of the result. Every path through the reducer must return the accumulator. Use it for genuine combining, and use a for...of loop when the body grows.

Worked examples

Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.

The same total, with and without an initial value

Three sums. The first two agree, which is why people drop the initial value; the third shows the case where dropping it would have thrown instead of answering.

1const numbers = [4, 8, 15];2 3console.log(numbers.reduce((total, n) => total + n, 0));4console.log(numbers.reduce((total, n) => total + n));5console.log([].reduce((total, n) => total + n, 0));
  • line 3

    Accumulator starts at 0 and the reducer runs three times, once per element.

  • line 4

    Accumulator starts as 4 — the first element — and the reducer runs only twice. Same answer here, but a different number of calls.

  • line 5

    0. Without the initial value this same line would throw a TypeError, because there is no first element to start from.

What it prints

27270

Watching the accumulator travel

The mechanism made visible. Printing inside a reducer is the fastest way to understand one you did not write — note how each line's accumulator is the previous line's sum.

1const numbers = [4, 8, 15];2 3const total = numbers.reduce((accumulator, current) => {4  console.log("acc=" + accumulator + " current=" + current);5  return accumulator + current;6}, 0);7 8console.log("total=" + total);
  • line 3

    A braced body, so the return on line 5 is compulsory. Delete it and total becomes undefined.

  • line 4

    Three lines will print, one per element. The accumulator on each is what the previous call returned.

What it prints

acc=0 current=4acc=4 current=8acc=12 current=15total=27

Counting each kind: the case only reduce handles

An object accumulator. This is the pattern worth memorising, because neither map nor filter can produce it and a loop version is barely shorter.

1const votes = ["red", "blue", "red", "green", "red"];2 3const tally = votes.reduce((counts, colour) => {4  counts[colour] = (counts[colour] || 0) + 1;5  return counts;6}, {});7 8console.log(tally);9console.log(tally.red);
  • line 4

    The first time a colour is seen, counts[colour] is undefined; || 0 turns that into 0 so the addition works.

  • line 5

    The line everybody forgets. Without it the next call receives undefined and the whole result collapses.

  • line 6

    The empty object is the initial value AND the announcement that this reduce builds an object.

What it prints

{"red":3,"blue":1,"green":1}3

The missing return, seen once on purpose

No error, no warning, no clue — just undefined. Meet it here so you recognise it in your own code in ten minutes' time.

1const numbers = [1, 2, 3];2 3const broken = numbers.reduce((total, n) => {4  total + n;5}, 0);6 7console.log(broken);
  • line 4

    The addition happens and the result is thrown away. The reducer returns undefined, which becomes the next accumulator.

  • line 7

    undefined. Compare with the working version: the only difference is the word return.

What it prints

undefined

Check yourself

Not scored, not stored. Getting one wrong is the useful part.

What happens when you call [].reduce((a, b) => a + b) with no initial value?

A one-element array reduced with no initial value. How many times does the reducer run, and what is printed?

let calls = 0;const answer = [7].reduce((a, b) => {  calls = calls + 1;  return a + b;});console.log(answer);console.log(calls);

In items.reduce((a, b) => ..., 0), what are a and b?

Write it yourself

Exercise · core · 7 tests

Average a list of numbers

Write a function called average that takes an array of numbers and returns their mean — the total divided by how many there are. Use reduce for the total. An empty array has an average of 0 (dividing by zero would give you something unusable, so this is a deliberate contract choice, not a mathematical claim).

What your code must do

average(numbers) returns a number: the sum of the elements divided by the count. average([2, 4, 6]) is 4. average([]) is 0 and must not be NaN. Fractional results are returned as they are, not rounded. The input array is unchanged.

Define average at the top level of your code — the tests call it by name.

Runs on your device in a sandbox with no network and no access to this page.

Exercise · stretch · 8 tests

Tally the items by category

Write a function called countByCategory that takes an array of item objects, each with a name and a category, and returns an object mapping each category to how many items had it. So three items in categories "fruit", "fruit" and "veg" give { fruit: 2, veg: 1 }. An empty array gives an empty object. A category that never appears must simply be absent from the result, not present with a count of 0.

What your code must do

countByCategory(items) returns a plain object whose keys are the categories that actually appeared and whose values are whole numbers greater than 0. countByCategory([]) is an object with no keys. The order of the keys is not part of the contract and is not tested. The input array and its objects are unchanged.

Define countByCategory at the top level of your code — the tests call it by name.

Runs on your device in a sandbox with no network and no access to this page.

Worth remembering

  • What is the accumulator in a reduce?

    The value carried from one call of the reducer to the next. It starts as the initial value, becomes whatever each call returns, and the last one returned is what reduce hands back. It is always the reducer's FIRST parameter.

  • Why should you always pass an initial value to reduce?

    Without one, an empty array throws a TypeError, the accumulator starts as the first element, and the reducer runs one fewer time. The initial value also declares the shape of the result — 0, {}, [] or "".

  • A reduce returns undefined. What is the first thing to check?

    A reducer with a braced body that does not return the accumulator. The next call then receives undefined and the result collapses, with no error to warn you.

Check this lesson against the source

We wrote the explanation; we did not invent the facts. This is the page that backs them.

MDN — Array.prototype.reduce()

Check there: The reducer takes the accumulator first and the current element second; with an initial value the accumulator starts there and iteration begins at index 0, without one it starts as the first element and begins at index 1; reduce on an empty array with no initial value throws a TypeError.

MDN — Array.prototype.join()

Check there: join builds a string from an array directly, so reduce is not needed for that case.

MDN — Math.max()

Check there: Math.max takes numbers as separate arguments, not an array — relevant when reducing to a maximum.

Prefer to read the source in a structured breakdown? Turn this doc into a breakdown

0 of 2 exercises in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences