I’m trying to loop through a JavaScript array to process some data, but I’m not sure which approach is best or why my current loop is skipping some items. I’ve tried for, forEach, and map, and now my logic is getting messy and hard to debug. Can someone explain the right way to loop through an array for performance and readability, and point out what I might be doing wrong?
When a loop in JS “skips” items, the cause is almost always one of these:
- You mutate the array while looping
- You use async code inside the loop and assume sync order
- You use
return,continue, orbreakin a way you did not intend
Here are the main loop options, and when to use them.
- Classic for loop
Best when you change the array length or need tight control.
Example that skips items by mistake:
for (let i = 0; i < arr.length; i++) {
if (shouldRemove(arr[i])) {
arr.splice(i, 1); // remove current
// i++ runs, so next item shifts into i, then you skip it
}
}
Safer version:
Option A, loop backwards:
for (let i = arr.length - 1; i >= 0; i--) {
if (shouldRemove(arr[i])) {
arr.splice(i, 1);
}
}
Option B, build a new array:
const result = [];
for (let i = 0; i < arr.length; i++) {
if (!shouldRemove(arr[i])) {
result.push(arr[i]);
}
}
forEach
Good for simple synchronous side effects. It does not support break or return to stop early.
Common issue:
arr.forEach(async item => {
await doStuff(item);
// code after forEach runs before these finish
});
If you use async, use for...of:
for (const item of arr) {
await doStuff(item);
}
map
Use when you want a new array from an old one. It should be pure and not used for side effects.
Bad pattern:
arr.map(item => {
if (!item.active) return; // returns undefined
doStuff(item); // side effect
});
Better pattern:
const processed = arr
.filter(item => item.active)
.map(item => transform(item));
for...of
Best general purpose loop for arrays now. Clear and works well with async.
for (const item of arr) {
if (!item) continue;
process(item);
}
If your loop skips items, check:
- Are you using
splice,push,shift, etc while looping forward - Are you returning early inside
maporfilterand expecting a side effect - Are you mixing async with
forEach - Are you filtering with
if (...) continuein a way that conflicts with your conditions
If you post the specific loop code, people can point to the exact line that causes the skips.
Your loop is “skipping” items for logical reasons, not because for, forEach, or map are secretly buggy.
@nachtdromer already covered the classic pitfalls (mutating while looping, async in forEach, etc.), so I’ll focus on how to structure the logic so you can’t easily shoot yourself in the foot.
1. Decide what you actually want from the loop
There are really only a few common goals:
- Transform each item → use
map - Keep / remove some items → use
filter - Produce a single result → use
reduce - Just “do stuff” with each item → use
for...of(orforEachif sync & simple)
If you mix these goals inside one loop, that’s when it gets messy and items start “disappearing” in ways that confuse you.
Bad mix example:
// transforms, filters, AND side effects in one go
const result = arr.map((item, i) => {
if (!item.active) return; // returns undefined
if (i % 2 === 0) arr.splice(i, 1); // mutates while looping
log(item); // side effect
return transform(item);
});
Looks clever, behaves terribly.
2. Split the logic into separate passes
Modern JS can afford a couple passes over an array. Usually the clarity win is worth the tiny perf hit.
Example: filter first, then transform, then maybe side effects.
const active = arr.filter(item => item.active);
const transformed = active.map(transform);
for (const item of transformed) {
doSideEffect(item);
}
Now there is basically no way to “skip” items by accident, and you’re not wrestling with indexes while mutating.
3. If you must mutate, isolate that too
I slightly disagree with treating forward mutation as something you just “fix” with clever indexing; in practice it’s easier to isolate the mutation step:
// 1) build the new array
const cleaned = [];
for (const item of arr) {
if (!shouldRemove(item)) {
cleaned.push(item);
}
}
// 2) replace the original in one go
arr.length = 0;
arr.push(...cleaned);
Yes, it’s more verbose, but your future self won’t be hunting some i++ bug at 2am.
4. Debug the skipping in 60 seconds
Drop a tiny tracer inside your loop to see why it’s skipping:
for (let i = 0; i < arr.length; i++) {
console.log('i:', i, 'value:', arr[i]);
if (!arr[i]) {
console.log('skipped: falsy item at index', i);
continue;
}
if (shouldRemove(arr[i])) {
console.log('removing index', i);
arr.splice(i, 1);
i--; // comment this out / in and see what happens
continue;
}
// rest of logic...
}
Once you log i and arr.length during the loop, you’ll usually see the “ohhh, it shifts and I never re-check that index” moment.
5. Strong defaults that rarely bite you
If you’re not doing anything exotic:
- Need async in order →
for...of+await - Need sync side effects only →
for...oforforEach - Need a new processed array →
filter+map - Avoid
splice/shift/unshiftinside any forward index-based loop unless you really know why
JS gives you a lot of ways to loop, but if the loop is skipping items, it’s almost always a logic issue in your conditions or mutations, not the choice of for vs forEach vs map.
If you paste the exact loop you’re using, folks can point at the specific line that’s betraying you.
The loop types are mostly covered already, so let’s zoom in on why your logic feels “messy” and how to cleanly structure it.
1. Stop thinking “which loop?”, start with “what shape is my result?”
Before you pick for, forEach, map, etc, answer:
- Do you want:
- a new array of processed items?
- the same array, but cleaned up?
- a single value (sum, object, map)?
- or just side effects (logging, network calls)?
If you mix these in one pass, bugs appear even if the loop type is “correct”.
Example of what not to do:
// transforms + removes + side effects in one pass
for (let i = 0; i < arr.length; i++) {
if (!arr[i].valid) {
arr.splice(i, 1);
continue;
}
arr[i] = transform(arr[i]);
doSideEffect(arr[i]);
}
This will eventually confuse you because the “remove vs transform vs side effect” logic is tangled.
Cleaner pattern:
- Decide: “First I clean the data, then I process it, then I do side effects.”
- Use a separate pass for each.
const cleaned = arr.filter(isValid);
const processed = cleaned.map(transform);
for (const item of processed) {
doSideEffect(item);
}
Yes, it is an extra pass or two, but in almost all real apps that cost is trivial compared to clarity.
I slightly disagree with @voyageurdubois here: looping backwards +
spliceworks, but it keeps you in index-micromanagement land. Splitting passes is easier to reason about for most people, especially when you come back in 3 months.
2. Check the shape of your conditions
A lot of “skipping items” is basically “my if conditions are doing more than I realized.”
Example:
for (const item of arr) {
if (!item.active) continue;
if (item.flagged) continue;
if (!item.data) continue;
process(item);
}
When debugging, collapse those conditions into named predicates:
const isProcessable = item =>
item.active &&
!item.flagged &&
!!item.data;
for (const item of arr) {
if (!isProcessable(item)) continue;
process(item);
}
Now if items are skipped, you can log just that helper:
for (const item of arr) {
const ok = isProcessable(item);
console.log('item:', item.id, 'processable?', ok);
if (!ok) continue;
process(item);
}
You will quickly catch the condition that is eliminating more than you intended.
3. Async + ordering: pick one model and stick to it
@nachtdromer already pointed out the forEach(async...) trap. Where I would add nuance:
-
If you care about order or rate limiting, use:
for (const item of arr) { await handleItem(item); } -
If you do not care about order and you want parallelism, don’t loop at all:
await Promise.all(arr.map(handleItem));
Mixing these ideas, like partially awaiting in a loop that also mutates the array, is where items “disappear” conceptually.
4. Avoid half-side-effect, half-transform map
One subtle anti-pattern that causes confusion:
const result = arr.map(item => {
if (!item.active) return;
process(item); // side effect
return transform(item);
});
Now result has undefined gaps, and you used map for side effects. You then later loop result and wonder why you see undefined.
Split it:
const active = arr.filter(item => item.active);
active.forEach(process);
const result = active.map(transform);
It reads like English, which is exactly what you want when tracking down “skipped” items.
5. A quick “skipping” checklist
When something vanishes from your loop, ask:
- Do I mutate
arrin the loop?- If yes, can I move that mutation into a separate pass?
- Do I have early
continue/returnbranches?- Wrap them into named predicates and log them.
- Is anything async?
- Choose either
for...of + await(sequential) orPromise.all(map())(parallel), but not a hybrid.
- Choose either
- Am I overloading one loop with multiple responsibilities?
- Split it. Filter then map; or build a new array then replace the old one.
Once you do this, the choice of for vs forEach vs map becomes almost a cosmetic decision.
On the “product” angle you mentioned, something like a dedicated “array processing utilities” module (let’s call it array-pipeline-toolkit) can make this kind of logic much more readable, acting as a small DSL over these patterns.
Pros of array-pipeline-toolkit:
- Encourages clear, pipeline-style operations (filter → map → reduce).
- Reduces accidental mutation by exposing mostly pure functions.
- Makes complex multi-step array flows easier to read and reason about.
Cons of array-pipeline-toolkit:
- Adds another abstraction layer you need to learn and maintain.
- Might be overkill for small scripts where plain loops are obvious.
- Can tempt you into chaining too much, hurting debuggability if misused.
Compared with what @voyageurdubois and @nachtdromer showed, a toolkit like that does not replace correct reasoning about loops and async, but it can enforce a more declarative style so your code is less prone to “skipping” bugs in the first place.
If you drop your current loop code, you can usually point to a single condition or splice that explains every skipped item.