In defense of the for...of loop
Despite its introduction in ES2015 and implementation in all browsers over 8.5 years ago, I still see .forEach
used in favor of the modern for...of
loop. Sadly, .forEach
is ill-suited for modern await
-centric code, and complicates control flow. It’s time to revisit this commonly banned syntax.
Async support
With the for...of
loop, proper async
support means that this code snippet
will run all fetch()
calls in sequence, then log "Done"
.
const paths = ["/a", "/b", "/c"];
for (const p of paths) {
await fetch(p);
}
console.log("Done");
Since .forEach
is unaware of async
functions, it will naively call each
async
function, ignore its promise value, and synchronously start the next
function. The result here is that every fetch()
will run in parallel, "Done"
will be logged likely before any of them finish, and we will have no idea when
any of the fetch()
calls have completed.
const paths = ["/a", "/b", "/c"];
paths.forEach(async (p) => {
await fetch(p);
});
console.log("Done");
Flow control
The for...of
loop supports all regular flow control in JS: continue
,
break
, and return
. Let’s examine this somewhat contrived function that uses
all three.
function find(items) {
for (const x of items) {
if (x.skip) {
// Skip to the next `x` from `items`
continue;
}
if (x.done) {
// Leave the `for...of` entirely
break;
}
if (x.value === target) {
// Leave the `find` function immediately
// without finishing the `for...of` loop
return x.value;
}
}
return null;
}
Because .forEach
uses a callback function, continue
and break
are
syntactically invalid. Worse, return
in this case is like continue
in the
for...of
example because there’s no way to exit .forEach
early.
function find(items) {
let ret = null;
let done = false;
items.forEach((x) => {
if (done) {
return;
}
if (x.skip) {
// Skip to the next `x` from `items`
return;
}
if (x.done) {
// Avoid processing any further `x` from `items`
done = true;
return;
}
if (x.value === target) {
ret = x.value;
done = true;
return;
}
});
return ret;
}
I’ve definitely seen people who argue against continue
, break
, and “early”
return
, but I think generally people aren’t against them. And I think the
.forEach
implementation of this function is pretty undeniably clunky for their
absence. Not to mention it’s strictly less efficient since it can’t avoid
processing the entire list.
Iterators and generators
If you’re not already using generators, I think you should consider it. I’ve
written about them before, and
MDN has a nice article
covering them. The .forEach
method is incapable of working with iterators, unless you convert them to arrays first. This removes all benefits of iterators
(primarily laziness and memory
consumption being decoupled from the collection size).
Isn’t for…of slower?
EDIT: I only tested in Firefox for this section. That was a mistake. When
testing in Edge and Safari, for...of
was actually as fast or fast than
.forEach
in general. Numeric for
still won in many tests, but I actually
found for small iteration counts that for...of
was winning in Edge. I will not
pretend to be a benchmarking expert here, and benchmarking languages with JIT
compilation and several implementations is extra tough. Thanks,
Tegan, for
pointing out conflicting benchmark results. I should probably test in the most
popular browsers when doing benchmarks. My mistake. (2025-03-11)
Maybe, but you’re not gonna notice the difference. And if you do, you should use
a numeric for
loop.
I used JS Benchmark to test the summation of 10 million numbers using both iteration techniques.
// SETUP CODE
const data = [...Array(10_000_000).keys()];
// TEST 1
let sum = 0;
for (const x of DATA) {
sum += x;
}
// TEST 2
let sum = 0;
DATA.forEach((x) => {
sum += x;
});
When adding 10 million numbers together, the difference was a measly 15 ms, or
around the time of one frame in a 60 fps application. It’s easy to throw around
percentages here to mislead people, like “.forEach
is 26% faster than
for...of
”, but I seriously doubt the actual time difference is meaningful in
most context for most programs.
Reducing the array of numbers from 10 million to 1 million changes the difference to around 2 ms. Testing 100 thousand elements gives a difference of about 0.1 ms.
I’m willing to bet you’re iterating closer to 10 elements on average, though. In this case I see the following results:
for...of
Ops/s: 12,770,982
Average run time: 0.000078 ms
.forEach
Ops/s: 35,682,598
Average run time: 0.000028 ms
So there’s maybe something to be said here that for...of
appears to have a
slower iteration speed and a larger fixed overhead for each run. But I’m willing
to bet that your time would be better spent making your React app not re-render
hundreds of times unnecessarily, shrinking your JS bundle, or going on a walk.
I’m not trying to trivialize performance critical JS, but I don’t think most of
us are in a position where worrying about this detail matters.
Finally, the speed of both of these approaches are simply dwarfed by a standard
numeric for
loop. And the regular for
loop, while clunky to write, supports
all of the correct behaviors of the for...in
loop (async
, flow control),
except iterators.
let sum = 0;
for (let i = 0; i < DATA.length; i++) {
sum += DATA[i];
}
for…of has been supported natively for nearly a decade
September 2016 is when the last* browser (Safari), impelmented ES2015, 8.5 years ago at the time of writing.
Because ES2015 introduced the iterator protocol and generators, and for...of
integrates with them natively, there was some concern about needing to use
massive polyfills in order to use for...of
in production. But there were
options to enable for...of
compilation that only supported array-like objects,
which didn’t bloat the compiled code.
And at this point, everyone connecting to your website has a browser that’s natively supported all of these features for years, so hopefully you’re not targeting ancient versions of JS with your Babel config or whatever compiler you’re using.
* Technically Edge took longer, but that’s the old version of Edge (pre-Chromium), which doesn’t exist any more. Let’s not worry about it.
Confusion between for…of and for…in
The related for...in
loop is a bit confusing. It should generally be avoided
in favor of a for...of
loop iterating the keys of an object:
for (const key in object) {
console.log(key);
}
for…in iterates over all enumerable properties, including those from the prototype chain. Given that prototype augmentation used to be more common in JS, this was deemed too risky and the newer Object.keys() method became the preferred way to get keys from an object.
Perhaps this common wisdom of avoiding for...in
mentally polluted people
against wanting to use for...of
due to confusion. Anecdotally, some people
can’t remember which is which.
React made everyone allergic to side effects
Because React has taken over the world and React insists
on using various techniques from functional programming, we have a growing
allergy to anything that looks like side effects in the JS community. Perhaps
.forEach
receives an undue reputation boost by being colocated with .map
,
.filter
, and .reduce
. I should write about how referential transparency does
not require a complete ban on immutability in the future.
Airbnb style guide strikes again
Prebuilt ESLint rule collections can be really influential. The Airbnb ESLint config package gets nearly 3.5 million weekly downloads. Airbnb’s style guide bans the use of for…of entirely, based on a strong emphasis for non-mutative array methods and a distaste for compiling generators to ES5. I would love to see them revisit this in the future due to their influence.
Style guides can offer a comforting consistency to code bases, but progress requires inconsistency as you migrate from old to new approaches.
Conclusion
I realize that generators were inefficient to support when targeting ES5 environments, but now that we’re 8.5 years removed from that era, I think we should revisit our coding practices.
Hopefully the caveats I’ve shown make it clear that for...of
is massively more
powerful and easier to read than .forEach
.
Further reading
The Code Barbarian has a great article about For vs forEach() vs for/in vs for/of in JavaScript that you can read as well. Hat tip to them for confirming my suspicion about the Airbnb style guide.