DEV Community

Cover image for When a `for...of` Loop Is Better Than `reduce()`
Amrishkhan Sheik Abdullah
Amrishkhan Sheik Abdullah

Posted on

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(...)
Enter fullscreen mode Exit fullscreen mode

Need grouping?

users.reduce(...)
Enter fullscreen mode Exit fullscreen mode

Need filtering?

users.reduce(...)
Enter fullscreen mode Exit fullscreen mode

Need transformation?

users.reduce(...)
Enter fullscreen mode Exit fullscreen mode

Need world peace?

Probably:

world.reduce(...)
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Sometimes a simple:

for...of
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

and suddenly realize:

It can do everything.
Enter fullscreen mode Exit fullscreen mode

And that's true.

The problem is:

Can do everything

≠

Should do everything
Enter fullscreen mode Exit fullscreen mode

Consider:

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

Reasonable.

Many teams would accept this.

Now consider:

const usersById = {}

for (const user of users) {
  usersById[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode

Question:

Which one is easier to understand?
Enter fullscreen mode Exit fullscreen mode

For many developers:

The loop.


Cognitive Load Matters

Most discussions around reduce focus on:

Elegance

Composability

Functional Style
Enter fullscreen mode Exit fullscreen mode

But software engineering has another important metric:

Cognitive Load
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Mental model:

Create object

Loop

Add items

Done
Enter fullscreen mode Exit fullscreen mode

Reading A Reduce

Now:

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

You must understand:

Accumulator

Initial State

Return Value

Reducer Contract
Enter fullscreen mode Exit fullscreen mode

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
    },
    []
  )
Enter fullscreen mode Exit fullscreen mode

Works.

But compare:

const activeUserNames = []

for (const user of users) {
  if (user.active) {
    activeUserNames.push(
      user.name
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Which is clearer?

Many developers would argue:

The loop.
Enter fullscreen mode Exit fullscreen mode

And that's okay.


The Hidden Mutation Problem

Ironically, many developers use reduce for functional purity.

Then immediately write:

acc.push(...)
Enter fullscreen mode Exit fullscreen mode

or:

acc[user.id] = user
Enter fullscreen mode Exit fullscreen mode

which mutates the accumulator.

Example:

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

At this point:

You're already mutating.
Enter fullscreen mode Exit fullscreen mode

So the purity argument disappears.

Now we're mainly comparing:

Reduce

vs

Loop
Enter fullscreen mode Exit fullscreen mode

on readability and performance.


The Immutable Version Is Often Worse

Some developers try:

users.reduce(
  (acc, user) => [
    ...acc,
    user.name
  ],
  []
)
Enter fullscreen mode Exit fullscreen mode

Looks functional.

But now:

New Array

Every Iteration
Enter fullscreen mode Exit fullscreen mode

Which can become extremely expensive.

Especially on large datasets.


Debugging Matters

Imagine debugging:

users.reduce(
  (acc, user) => {
    ...
  },
  {}
)
Enter fullscreen mode Exit fullscreen mode

versus:

for (const user of users) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Where do most developers place breakpoints?

Usually:

Inside the loop.
Enter fullscreen mode Exit fullscreen mode

Loops are naturally procedural.

Debuggers love procedural code.


Performance Matters Too

From the previous article:

Functional Code Can Be Slower
Enter fullscreen mode Exit fullscreen mode

Many reduce implementations involve:

  • callbacks
  • closures
  • allocations
  • intermediate objects

Example:

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

This looks elegant.

But benchmarks terribly.

Compare:

const result = {}

for (const user of users) {
  result[user.id] = user
}
Enter fullscreen mode Exit fullscreen mode

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
  )
Enter fullscreen mode Exit fullscreen mode

Perfect.


Scenario 2: State Evolution

events.reduce(
  reducer,
  initialState
)
Enter fullscreen mode Exit fullscreen mode

This is literally what reducers were designed for.


Scenario 3: Event Sourcing

From the previous article:

events.reduce(
  accountReducer,
  initialState
)
Enter fullscreen mode Exit fullscreen mode

Excellent fit.


Scenario 4: Generic Reduction Logic

reduce()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Clear.

Fast.

Obvious.


Scenario 2: Complex Conditional Logic

for (const user of users) {
  if (...) {
    ...
  } else if (...) {
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Much easier to follow.


Scenario 3: Multiple Side Effects

for (const order of orders) {
  updateCache(order)
  logOrder(order)
  notify(order)
}
Enter fullscreen mode Exit fullscreen mode

Trying to force this into reduce often becomes awkward.


Scenario 4: Early Exit

for (const user of users) {
  if (user.id === targetId) {
    break
  }
}
Enter fullscreen mode Exit fullscreen mode

Reduce cannot naturally stop.

Loops can.


The Senior Developer Perspective

Junior developers often ask:

Which is better?
Enter fullscreen mode Exit fullscreen mode

Senior developers ask:

Which is clearer?
Enter fullscreen mode Exit fullscreen mode

Because maintainability usually dominates.

Not cleverness.

Not elegance.

Not theoretical purity.


The Most Important Rule

Don't optimize for:

Shortest Code
Enter fullscreen mode Exit fullscreen mode

Optimize for:

Most Understandable Code
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

is exactly right.

Sometimes:

for...of
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)