Part 8 — Control Flow
Conditionals, Loops, break, continue, labels, Iteration method comparison 🎯, Recursion
Conditionals#
if (cond) { } else if (other) { } else { }
switch (fruit) {
case "apple":
case "pear": // fall-through: apple & pear share this
console.log("pome"); break;
default:
console.log("other");
}
⚠️ switch uses strict comparison (===) and falls through without break. Forgetting break is a classic bug. For value-mapping, an object lookup is often cleaner:
const label = { apple: "pome", pear: "pome" }[fruit] ?? "other";
Loops#
for (let i = 0; i < n; i++) { }
while (cond) { }
do { } while (cond); // runs body at least once
for (const x of iterable) { } // 🟢 values (arrays, strings, Maps, Sets, generators)
for (const k in obj) { } // ⚠️ keys, INCLUDING inherited enumerable ones
⚠️ for...in traps: iterates keys as strings, includes inherited properties, and order isn't guaranteed for integer-like keys the way you'd expect. Use it for plain objects only, guard with Object.hasOwn(obj, k), or better use Object.keys/entries. Never use for...in to iterate arrays.
break, continue, labels#
outer:
for (const row of grid) {
for (const cell of row) {
if (cell === target) break outer; // break both loops
if (cell < 0) continue outer;
}
}
🟡 Labels are legitimate for breaking nested loops but often signal that logic should move into a function that can return.
Iteration method comparison 🎯#
| Construct | Iterates | Can break? |
Returns | Async-friendly | Notes |
|---|---|---|---|---|---|
for |
anything | yes | — | with await inside |
most control, fastest |
for...of |
iterables | yes | — | ✅ (for await...of) |
clean, supports break/continue |
for...in |
enumerable keys | yes | — | — | objects only; includes inherited |
forEach |
array | no ⚠️ | undefined | ❌ ignores await |
can't break; skips holes |
map |
array | no | new array | ❌ | transform; don't use for side-effects only |
filter |
array | no | subset array | ❌ | keep where callback truthy |
reduce |
array | no | single value | ❌ | fold; powerful but can hurt readability |
⚠️ You cannot break out of forEach, and await inside forEach does not pause the loop. For sequential async, use for...of with await.
// ❌ doesn't await
items.forEach(async (i) => { await save(i); });
// ✅ sequential
for (const i of items) { await save(i); }
// ✅ parallel
await Promise.all(items.map(i => save(i)));
Recursion#
ELI12. A function that calls itself, shrinking the problem each time until a "base case" stops it.
function factorial(n) {
if (n <= 1) return 1; // base case — always required
return n * factorial(n - 1);
}
⚠️ No base case → infinite recursion → RangeError: Maximum call stack size exceeded. JS engines generally do not optimize tail calls (spec'd but unimplemented in V8), so deep recursion can overflow — convert to iteration or an explicit stack for large inputs.
Interview questions (Part 8):
- Why can't you
breakout offorEach? What do you use instead? for...invsfor...of?- What happens with
awaitinsideforEach? Fix it two ways (sequential/parallel).