JavaScript Conditionals and Loops

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

Node.js 22 LTS

The syntax of branching and looping is short. The bugs come from choosing the wrong form: wrong results while iterating, break that does not break, keys inherited from a prototype.

if / else and the conditional operator

const score = 85;

if (score >= 90) {
  console.log("excellent");
} else if (score >= 60) {
  console.log("pass");
} else {
  console.log("fail");
}

When there are exactly two branches and both produce a value, the conditional operator is shorter:

const label = score >= 60 ? "pass" : "fail";

More than one level of nesting and you should go back to if. Readability wins over line count.

switch for equality matching

const status = "pending";

switch (status) {
  case "pending":
    console.log("waiting");
    break;
  case "done":
    console.log("finished");
    break;
  default:
    console.log("unknown");
}

Two rules:

  1. switch compares with ===, so "1" does not match 1.
  2. Every case needs break, or execution falls through to the next one. Intentional fall-through should carry a comment saying so.

For mapping a value to a result, an object lookup is often cleaner than switch:

const MESSAGES = { pending: "waiting", done: "finished" };
const message = MESSAGES[status] ?? "unknown";

for: when you need the index

const names = ["ann", "bob", "cindy"];

for (let i = 0; i < names.length; i++) {
  console.log(i, names[i]);
}

Caching names.length in a variable no longer helps performance in modern engines; do not trade readability for it.

for…of: when you only need values

for (const name of names) {
  console.log(name);
}

for...of iterates values and works on any iterable: arrays, strings, Set, Map, generators. It cannot go out of bounds and expresses intent better than an index loop.

If you need both, use entries():

for (const [i, name] of names.entries()) {
  console.log(i, name);
}

for…in: not for arrays

for...in iterates enumerable property names, including inherited ones:

const arr = ["a", "b"];
arr.custom = "x";

for (const key in arr) {
  console.log(key);   // "0", "1", "custom"
}

Note that the keys are strings. Sorting is not guaranteed either. Iterate arrays with for...of or array methods.

For plain objects, for...in works but should be guarded:

const user = { name: "ann", age: 30 };

for (const key in user) {
  if (Object.hasOwn(user, key)) {
    console.log(key, user[key]);
  }
}

In most cases Object.keys(user) or Object.entries(user) is clearer, because you get a real array and can use map / filter on it.

while and do…while

while fits loops whose iteration count is unknown up front:

let retry = 0;
while (retry < 3) {
  console.log(`attempt ${retry + 1}`);
  retry++;
}

do...while runs the body at least once, which suits “read input, then decide whether to repeat”.

A while loop without a state change hangs the process. Always identify what makes the condition false before writing the body.

break and continue

for (const n of [1, 2, 3, 4, 5]) {
  if (n === 2) continue;   // skip this iteration
  if (n === 4) break;      // exit the loop
  console.log(n);
}
// 1 3

Neither works inside forEach:

[1, 2, 3].forEach((n) => {
  if (n === 2) break;      // SyntaxError: Illegal break statement
});

Inside forEach, return skips the current item (like continue). To stop early, use for...of with break, or a method with short-circuit semantics: some(), every(), find(), findIndex().

const hasNegative = nums.some((n) => n < 0);   // stops at the first match
const firstEven = nums.find((n) => n % 2 === 0);

Labels for nested loops

outer:
for (const row of [[1, 2], [3, 4]]) {
  for (const n of row) {
    if (n === 3) break outer;   // exits both loops
    console.log(n);
  }
}
// 1 2

Labels are rare, but when you need to leave two levels at once they beat a boolean flag.

A worked example

const orders = [
  { id: 1, amount: 120, paid: true },
  { id: 2, amount: 80, paid: false },
  { id: 3, amount: 200, paid: true },
];

let total = 0;
let firstUnpaid = null;

for (const order of orders) {
  if (!order.paid) {
    firstUnpaid ??= order;   // record only the first one
    continue;
  }
  total += order.amount;
}

console.log(total);            // 320
console.log(firstUnpaid.id);   // 2

The same logic with array methods:

const total = orders.filter((o) => o.paid).reduce((sum, o) => sum + o.amount, 0);
const firstUnpaid = orders.find((o) => !o.paid);

Prefer the second version when the operations map cleanly; keep the loop when you need both results in one pass or early exit.

Common mistakes

1. Missing break in switch

Two cases run and you get a result that looks half-applied.

2. for...in on an array

String indices, extra properties, no ordering guarantee.

3. break inside forEach

Syntax error. Use for...of, some, or find.

4. while without updating the condition

The script hangs; in a server it blocks every request on that worker.

5. Splicing while iterating

for (let i = 0; i < items.length; i++) {
  if (items[i].stale) items.splice(i, 1);   // skips the next element
}

Filter into a new array, or iterate backwards when mutating in place.

6. Off-by-one in index loops

i <= arr.length reads one element past the end. Use for...of and the problem disappears.

Summary

  • Index loops for positions; for...of for values; entries() for both.
  • for...in iterates keys, and never belongs on an array.
  • switch uses ===; write break or use an object lookup.
  • forEach cannot be interrupted; use for...of or a short-circuiting method.
  • Never mutate the collection you are iterating.

Next: functions, scope chains, and closures.