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.
Top comments (0)