hand-rolled counters and tangled break / continue
for-Loop Control Flow
When you drive iteration by hand — a counter, a captured variable, an in-place edit — the loop and the data fall out of sync.
Manual loop control invites the index and the collection to disagree.
01in the wild
In the wild
Closure Captures the Loop Variable
Every closure created in the loop shares one binding, so they all see its final value.
example.js
// SMELL: var shares ONE binding across the whole loop
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
fns.map(f => f()); // [3, 3, 3]
// RIGHT: let gives each iteration its own binding
for (let i = 0; i < 3; i++) {
fns.push(() => i);
}
fns.map(f => f()); // [0, 1, 2]var is one binding mutated in place; every closure reads its last value. let is fresh per iteration.
// observed
var: [3, 3, 3] let: [0, 1, 2]
example.py
# SMELL: lambdas close over the variable, not its value
fns = [lambda: i for i in range(3)]
[f() for f in fns] # [2, 2, 2]
# RIGHT: bind the current value as a default argument
fns = [lambda i=i: i for i in range(3)]
[f() for f in fns] # [0, 1, 2]Python closures capture the variable by reference. A default argument snapshots the value at definition time.
// observed
late-binding: [2, 2, 2] snapshot: [0, 1, 2]
Mutating a Collection While Iterating
Removing from the very list you are looping over makes the cursor skip elements.
example.py
# SMELL: deleting while iterating shifts items under the cursor
nums = [1, 2, 2, 3]
for n in nums:
if n == 2:
nums.remove(n) # skips the second 2
# nums -> [1, 2, 3]
# RIGHT: build a new list; never mutate what you iterate
nums = [n for n in nums if n != 2] # [1, 3]remove() shifts every later element down one, so the loop's index jumps past the next match.
// observed
in-place: [1, 2, 3] (one 2 survives) filter: [1, 3]
example.java
// SMELL: structural change during iteration -> exception
for (String s : list) {
if (s.isBlank()) list.remove(s); // ConcurrentModificationException
}
// RIGHT: removeIf, or an explicit Iterator.remove()
list.removeIf(String::isBlank);The for-each iterator fails fast when the collection changes under it. removeIf mutates safely in one pass.
// observed
for-each: ConcurrentModificationException removeIf: blanks removed cleanly
02cross-pollination
Where this compounds
Nondeterminism
- Concurrent Modification During Iteration × Threading & Mutexes
Runtime Errors
- Unbounded Input → Quadratic Blowup × Unconstrained Inputs
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.