Dev.to WebDev 🛠 Dev 👁 1 📖 5 min read

When a `for...of` Loop Is Better Than `reduce()`

A few years ago, I was obsessed with reduce(). Every problem looked like a reduction problem. Need a lookup table? users.reduce(...) Need grouping? users.reduce(...) Need filtering? users.reduce(..

A few years ago, I was obsessed with reduce().

Every problem looked like a reduction problem.

Need a lookup table?

users.reduce(...)

Need grouping?

users.reduce(...)

Need filtering?

users.reduce(...)

Need transformation?

users.reduce(...)

Need world peace?

Probably:

world.reduce(...)

Eventually I realized something.

The problem wasn't reduce().

The problem was me.

Because while reduce() is incredibly powerful, many developers eventually reach a stage where they start asking a more important question:

Should I even be using reduce() here?

And surprisingly often, the answer is:

No.

Sometimes a simple:

for...of

is easier to read.

Easier to debug.

Faster.

And ultimately the better engineering choice.

The Problem With Loving reduce() Too Much

Developers often discover:

reduce()

and suddenly realize:

It can do everything.

And that's true.

The problem is:

Can do everything

≠

Should do everything

Consider:

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

Reasonable.

Many teams would accept this.

Now consider:

const usersById = {}

for (const user of users) {
  usersById[user.id] = user
}

Question:

Which one is easier to understand?

For many developers:

The loop.

Cognitive Load Matters

Most discussions around reduce focus on:

Elegance

Composability

Functional Style

But software engineering has another important metric:

Cognitive Load

How much mental effort does it take to understand the code?

Reading A Loop

Most developers instantly understand:

const result = {}

for (const user of users) {
  result[user.id] = user
}

Mental model:

Create object

Loop

Add items

Done

Reading A Reduce

Now:

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

You must understand:

Accumulator

Initial State

Return Value

Reducer Contract

Not difficult.

But undeniably more concepts.

The "Everything Is Reduce" Trap

Consider:

const activeUserNames =
  users.reduce(
    (acc, user) => {
      if (user.active) {
        acc.push(user.name)
      }

      return acc
    },
    []
  )

Works.

But compare:

const activeUserNames = []

for (const user of users) {
  if (user.active) {
    activeUserNames.push(
      user.name
    )
  }
}

Which is clearer?

Many developers would argue:

The loop.

And that's okay.

The Hidden Mutation Problem

Ironically, many developers use reduce for functional purity.

Then immediately write:

acc.push(...)

or:

acc[user.id] = user

which mutates the accumulator.

Example:

users.reduce(
  (acc, user) => {
    acc.push(user.name)
    return acc
  },
  []
)

At this point:

You're already mutating.

So the purity argument disappears.

Now we're mainly comparing:

Reduce

vs

Loop

on readability and performance.

The Immutable Version Is Often Worse

Some developers try:

users.reduce(
  (acc, user) => [
    ...acc,
    user.name
  ],
  []
)

Looks functional.

But now:

New Array

Every Iteration

Which can become extremely expensive.

Especially on large datasets.

Debugging Matters

Imagine debugging:

users.reduce(
  (acc, user) => {
    ...
  },
  {}
)

versus:

for (const user of users) {
  ...
}

Where do most developers place breakpoints?

Usually:

Inside the loop.

Loops are naturally procedural.

Debuggers love procedural code.

Performance Matters Too

From the previous article:

Functional Code Can Be Slower

Many reduce implementations involve:

  • callbacks
  • closures
  • allocations
  • intermediate objects

Example:

users.reduce(
  (acc, user) => ({
    ...acc,
    [user.id]: user
  }),
  {}
)

This looks elegant.

But benchmarks terribly.

Compare:

const result = {}

for (const user of users) {
  result[user.id] = user
}

Usually much faster.

When reduce() Shines

This article is not anti-reduce.

Far from it.

There are scenarios where reduce is clearly superior.

Scenario 1: Mathematical Aggregation

const total =
  prices.reduce(
    (sum, price) =>
      sum + price,
    0
  )

Perfect.

Scenario 2: State Evolution

events.reduce(
  reducer,
  initialState
)

This is literally what reducers were designed for.

Scenario 3: Event Sourcing

From the previous article:

events.reduce(
  accountReducer,
  initialState
)

Excellent fit.

Scenario 4: Generic Reduction Logic

reduce()

expresses intent very clearly.

You're reducing many values into one.

When for...of Shines

Scenario 1: Building Objects

const map = {}

for (const item of items) {
  map[item.id] = item
}

Clear.

Fast.

Obvious.

Scenario 2: Complex Conditional Logic

for (const user of users) {
  if (...) {
    ...
  } else if (...) {
    ...
  }
}

Much easier to follow.

Scenario 3: Multiple Side Effects

for (const order of orders) {
  updateCache(order)
  logOrder(order)
  notify(order)
}

Trying to force this into reduce often becomes awkward.

Scenario 4: Early Exit

for (const user of users) {
  if (user.id === targetId) {
    break
  }
}

Reduce cannot naturally stop.

Loops can.

The Senior Developer Perspective

Junior developers often ask:

Which is better?

Senior developers ask:

Which is clearer?

Because maintainability usually dominates.

Not cleverness.

Not elegance.

Not theoretical purity.

The Most Important Rule

Don't optimize for:

Shortest Code

Optimize for:

Most Understandable Code

Future you will thank you.

Your teammates will thank you.

Your debugger will thank you.

Pros Of reduce()

1. Expresses Reduction Clearly

2. Great For State Evolution

3. Excellent For Event Processing

4. Composable

5. Powerful Abstraction

Cons Of reduce()

1. Higher Cognitive Load

2. Easy To Abuse

3. Can Hide Complexity

4. Harder To Debug

5. Often Slower Than Loops

Pros Of for...of

1. Extremely Readable

2. Easy To Debug

3. Supports Early Exit

4. Usually Faster

5. Lower Cognitive Overhead

Cons Of for...of

1. Less Declarative

2. Easier To Introduce Side Effects

3. More Boilerplate

4. Harder To Compose

5. Can Become Procedural Spaghetti

If misused.

The Real Lesson

The biggest mistake developers make with reduce() is treating it like a badge of sophistication.

It's not.

It's a tool.

A very powerful tool.

But still a tool.

Sometimes:

reduce()

is exactly right.

Sometimes:

for...of

is exactly right.

The best engineers don't pick sides.

They pick the clearest solution for the problem in front of them.

And surprisingly often, that solution looks a lot less clever than you might expect.

What's Next?

In the next article we'll discuss:

The Hidden Cost Of Object Spread

Because one innocent-looking pattern:

{
  ...obj,
  newValue
}

has probably caused more accidental JavaScript performance issues than most developers realize.

About The Author

Hi, I'm Amrish Khan.

I enjoy building developer tools, exploring software architecture, and writing about the deeper ideas behind everyday programming concepts.

I'm also building Aruvix — a growing ecosystem of local-first developer tools designed to process data directly in the browser without unnecessary uploads.

Here's a detailed blog on Aruvix:

https://dev.to/amrishkhan05/aruvix-the-ultimate-offline-first-developer-toolkit-e0i

You can follow my work and thoughts here:

Portfolio:
https://www.amrishkhan.dev

LinkedIn:
https://www.linkedin.com/in/amrishkhan

GitHub:
https://www.github.com/amrishkhan05

If you enjoyed this article, consider following for more deep dives into JavaScript, architecture, local-first software, and performance engineering.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.