Need help understanding how to use JavaScript reduce correctly

I’m struggling to understand how JavaScript’s reduce method really works in practical use. My array callbacks keep returning unexpected results or undefined, and I’m not sure when I should use an initial value or how to properly accumulate objects instead of just numbers. Can someone walk me through a few clear examples and common mistakes so I can see what I’m doing wrong and how to fix it?

Think of reduce as a loop that always carries two things:

  1. accumulator
  2. current item

Its signature:

array.reduce((acc, curr, index, array) => {
// return new acc
}, initialValue)

Key rules:

  1. If you give an initial value
    Then on first call:
    acc = initialValue
    curr = array[0]

  2. If you skip initial value
    Then on first call:
    acc = array[0]
    curr = array[1]
    And reduce will throw if the array is empty.

Most bugs come from:

• Forgetting to return acc in the callback
• Using the wrong initial value
• Expecting reduce to mutate acc automatically

Example 1: sum numbers

const nums = [1, 2, 3];

const sum = nums.reduce((acc, n) => {
return acc + n;
}, 0);

console.log(sum); // 6

If you forget 0:

const sum2 = nums.reduce((acc, n) => acc + n);
// still works here: acc starts as 1, then 1+2+3

But on an empty array:

.reduce((a, n) => a + n); // throws

With 0 it works:

.reduce((a, n) => a + n, 0); // 0

So use an initial value when:

• The array might be empty
• The type of the result is not the same as the array items
• You want predictable behavior

Example 2: build an object

const users = [
{ id: 1, name: ‘Ann’ },
{ id: 2, name: ‘Bob’ }
];

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

Result:

{
1: { id: 1, name: ‘Ann’ },
2: { id: 2, name: ‘Bob’ }
}

Notice the initial value is {}
Without that, acc would be the first user object, which is wrong.

Example 3: flatten array

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

const flat = nested.reduce((acc, arr) => {
return acc.concat(arr);
}, );

Result: [1, 2, 3, 4, 5]

Again, initial value is [] because we want an array of all numbers, not start from [1, 2] as the first acc value.

Why you get undefined:

This code often causes it:

const result = arr.reduce((acc, item) => {
acc.push(item * 2);
// forgot to return acc
}, );

Then result is undefined, because your callback returned undefined on the first iteration, so acc becomes undefined.

Fix:

const result = arr.reduce((acc, item) => {
acc.push(item * 2);
return acc;
}, );

When you should use reduce:

• When you want one final value from a list
Sum, product, max, min
Combined object, merged arrays, etc

When you should avoid it:

• When you only want to transform each item, use map
• When you only want to filter items, use filter
• When reduce makes the code harder to read

Quick pattern checklist:

Sum:

arr.reduce((acc, x) => acc + x, 0);

Count:

arr.reduce((acc, x) => acc + 1, 0);

Group by:

arr.reduce((acc, item) => {
const key = item.type;
if (!acc[key]) acc[key] = ;
acc[key].push(item);
return acc;
}, {});

If you post one of your reduce callbacks that returns unexpected results, people can point at the exact place where it goes off, but 9 out of 10 times it is either missing return or wrong initial value.

Think of reduce as two separate questions:

  1. What shape do I want at the end?
  2. How do I get from “current accumulator + current item” to that shape?

@viajantedoceu already nailed the mechanics, so I’ll focus on the mental model that usually fixes the “undefined” and “huh???” moments.


1. Pretend reduce is just a loop you’d write by hand

Take this manual loop:

let acc = 0;
for (const n of nums) {
  acc = acc + n;
}

reduce is literally just:

const sum = nums.reduce((acc, n) => acc + n, 0);

If you can’t easily write the for loop version, your reduce is probably going to be confusing too. So a good strategy:

  1. First write it with a for loop.
  2. When it works, translate to reduce.

Example: turning an array into a string:

// loop version
let result = 'Total: ';
for (const n of nums) {
  result += n + ',';
}

// reduce version
const result = nums.reduce((acc, n) => acc + n + ',', 'Total: ');

Notice the initial value is 'Total: ' because that’s what I would assign result to before the loop.


2. Use initialValue almost always

I’ll slightly disagree with folks who say skipping it is fine: technically true, but in practice it causes subtle bugs.

Use an initial value if:

  • The result type is different from the element type
    • array → object
    • array → string
    • array → number but items are objects, etc.
  • The array might be empty
  • You just don’t want to think about the weird “first element becomes acc” rule

Example that bites a lot of people:

const items = [];

const total = items.reduce((acc, item) => acc + item.price);
// TypeError if items is empty

Fixed:

const total = items.reduce((acc, item) => acc + item.price, 0);

My personal rule: if I have to pause and think ‘what is acc on the first iteration?’, I just give an initial value and move on with my life.


3. Why you get undefined results

The classic mistake:

const doubled = arr.reduce((acc, x) => {
  acc.push(x * 2);
  // no return!
}, []);

First iteration returns undefined, so acc becomes undefined in the next step, and the final result from reduce is undefined.

General pattern when you mutate acc:

const result = arr.reduce((acc, item) => {
  // mutate acc
  acc.push(item * 2);
  // and ALWAYS return it
  return acc;
}, []);

If your reduce returns undefined, 9/10 times you either:

  • forgot return, or
  • returned something else by mistake

4. “When should I actually use reduce?”

Concrete examples that feel natural:

a) Combine an array into a different structure

Group items by a key:

const byCategory = products.reduce((acc, product) => {
  const key = product.category;
  if (!acc[key]) acc[key] = [];
  acc[key].push(product);
  return acc;
}, {});

Array of objects → object of arrays. Perfect reduce job.

b) Build a map of ids to objects

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

c) Run some calculation that depends on previous state

Like keeping track of a running maximum, minimum, or custom score:

const maxScore = scores.reduce((max, s) => s > max ? s : max, -Infinity);

5. When not to use reduce

If all you’re doing is:

  • “turn each item into something else” → use map
  • “keep some items, drop others” → use filter
  • “loop for side effects” → use forEach or a plain loop

reduce is for “I have a bunch of things and I want exactly one final thing at the end.”

If you catch yourself stuffing complicated logic, conditions, and multiple responsibilities inside reduce, it’ll be harder to read than just writing a boring for loop.


6. Quick debugging trick

When your result is weird, temporarily log everything:

const result = arr.reduce((acc, item, index) => {
  console.log('index:', index, 'acc:', acc, 'item:', item);
  // do stuff
  return acc;
}, initialValue);

Watch what acc and item are on each step. That usually makes the bug obvious.


If you want, post one of your reduce snippets that’s giving you undefined and we can rewrite it both as a loop and as a clean reduce, step by step.

Think of reduce in terms of shapes rather than mechanics:

  • Input shape: T[]
  • Output shape: X (number, string, object, array, whatever)

The whole job of reduce is: “Given acc of type X and item of type T, how do I get a new X?”

@codecrafter and @viajantedoceu nailed the mechanics and the classic bugs (missing return, bad initial value), so I’ll focus on patterns where people misuse reduce and how to rewrite them.


1. When your reduce is secretly a pipeline

If you catch yourself chaining maps and filters inside a single reduce, it is usually clearer to split it:

Bad pattern:

const result = arr.reduce((acc, item) => {
  if (item.active) {
    acc.push(item.value * 2);
  }
  return acc;
}, []);

Cleaner pattern:

const result = arr
  .filter(item => item.active)
  .map(item => item.value * 2);

Use reduce only when you truly need a custom “accumulator” shape or logic that cannot be expressed as simple map/filter.


2. When you should probably bail out of reduce

If you are doing any of this:

  • Multiple unrelated counters or flags in one accumulator
  • Deeply nested if / switch inside reduce
  • Side effects like DOM updates, logging, API calls

You usually get better readability with a plain for loop. Performance wise, reduce is not magic. It is just a loop in disguise, so do not hesitate to drop it if your callback logic feels too crowded.


3. Initial value rule of thumb that slightly contradicts others

@codecrafter suggests “use an initial value when the type is different or array might be empty.” I’d actually push it further:

If acc is not obviously the same type as item, always provide an initial value.

So:

  • numbers.reduce((a, n) => a + n, 0) → fine
  • numbers.reduce((a, n) => a + n) → I personally avoid, even if it works
  • objects.reduce(...) where acc is {}, [], or a string → never skip initial

This avoids the “first element becomes acc” mental overhead and weird edge cases like single element arrays acting differently than multi element ones.


4. Debugging reduce without going insane

Instead of staring at the final undefined, turn it into a stepwise log:

const result = arr.reduce((acc, item, index) => {
  console.log({ index, acc, item });
  // your logic here
  return acc;
}, initialValue);

If acc suddenly becomes undefined, you know exactly which step killed it and can inspect what you returned there.

Also, do not forget your callback can have a default:

const result = arr.reduce((acc = [], item) => {
  // this will not fix a missing return,
  // but can protect from some weird calls
  return acc;
}, []);

I would not rely on this pattern heavily, but it is handy in some defensive code.


5. Mental pattern: “accumulator as a mini state machine”

Instead of viewing acc as just “the thing I’m building,” treat it like state:

Example: summarizing stats

const stats = nums.reduce((state, n) => {
  return {
    count: state.count + 1,
    sum: state.sum + n,
    max: n > state.max ? n : state.max
  };
}, { count: 0, sum: 0, max: -Infinity });

Here state is like a tiny state machine evolving on each item.
This pattern scales well when you need more complex results and avoids mixing random variables outside the reducer.


6. Quick checklist before you write a reducer

Ask yourself:

  1. What is the final type and shape I want?
    Example: “an object keyed by category with arrays of items”

  2. What is the initial shape for that?
    {}, [], 0, ', or a custom object

  3. Given (acc, item), can I write a pure function that:

    • does not depend on globals
    • does not mutate things you do not own
    • returns the next acc every time

If you answer these 3 clearly, your reducer almost writes itself.


7. On the mysterious product title '

This empty product title is actually a perfect analogy to bad reduces:

Pros:

  • No expectations: like a reducer without an initial value, it forces you to think about what you really want the output to be.
  • Flexible: you can assign any mental label to it, similar to how your accumulator type can be anything you define.

Cons:

  • Zero self documentation: just like a reducer with a vague accumulator name (acc is fine, but x is not), ' tells you nothing about its purpose.
  • Hard to search: in code, a reducer that does too much at once is as hard to “search with your eyes” as an unnamed product is to search in a store.

Name your accumulators well and give them a clear “product title” so your future self knows what the reducer is actually building.


8. How this complements @codecrafter and @viajantedoceu

  • @codecrafter focused on core rules and classic mistakes. Use that as your “reduce reference sheet.”
  • @viajantedoceu emphasized mental model and translating from for loops. That is your “training wheels.”

Layer this on top:

  • Treat reduce as state evolution, not just folding.
  • Prefer initial values more aggressively than “only when needed.”
  • Abort to a for loop as soon as readability drops.

If you post one concrete reduce you are struggling with, you can walk it through these three views:

  1. Simple for loop version
  2. Clear accumulator “shape” definition
  3. Stepwise logging to see where it derails