Loops
for, for-of, for-in, forEach - which to use and why forEach can't be awaited.
JavaScript offers several loop constructs. Picking the right one matters for correctness, readability, and async behavior.
A classic for loop gives full control over the index - useful when you need the position, not just the value.
const items = ["a", "b", "c"];
for (let i = 0; i < items.length; i++) {
console.log(i, items[i]);
}
// 0 a
// 1 b
// 2 cfor-of iterates over the values of any iterable - arrays, strings, Sets, Maps, and generators. It's the modern default for sequential iteration.
const colors = ["red", "green", "blue"];
for (const color of colors) {
console.log(color);
}
// red
// green
// bluefor-in iterates over the keys of an object - including enumerable inherited properties. Avoid it on arrays; it can surface unexpected prototype keys and the order is not guaranteed.
const user = { name: "Ana", role: "admin" };
for (const key in user) {
console.log(key, user[key]);
}
// name Ana
// role admin.forEach passes each value to a callback but cannot be break-ed out of and cannot be awaited. for-of is safer in both situations.
const ids = [1, 2, 3];
// WRONG - forEach does not await the async callback
ids.forEach(async (id) => {
await saveToDb(id); // runs fire-and-forget
});
// CORRECT - sequential async iteration
for (const id of ids) {
await saveToDb(id);
}
// CORRECT - parallel with bounded result collection
await Promise.all(ids.map((id) => saveToDb(id)));In production
The forEach + async trap is one of the most common async bugs in Node.js codebases: the function returns immediately while the callbacks run in the background, so callers believe the work is done before it is. Use for-of with await for serial processing, or Promise.all with .map for parallel - and be explicit about which concurrency model you want. Unbounded Promise.all across 500 records can saturate a database connection pool just as effectively as a bug.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free