JavaScript Control Flow


  • Description: Conditionals (if, switch, ternary), loops (for, while, for…of, for…in), labels, break/continue, and modern iteration helpers.
  • My Notion Note ID: K2A-F4-3
  • Created: 2018-03-23
  • Updated: 2026-05-17
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Conditionals

1.1 if / else

if (cond) {
  // ...
} else if (other) {
  // ...
} else {
  // ...
}

Braces are optional for single statements but always preferred — saves the "dangling else" bug.

if (cond) doThing();          // works, but vulnerable to later edits
if (cond) { doThing(); }       // safe

The condition gets coerced to boolean — see falsy values in javascript-types-and-operators.

1.2 Ternary

const label = isReady ? 'Go' : 'Wait';

// Nested — readable when each branch fits one line
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : 'C';

Inline-only — use if/else when branches have side effects.

1.3 Guard Clauses

Early returns flatten code and reduce nesting:

function process(user) {
  if (!user) return;
  if (!user.active) return;
  if (user.balance < 0) throw new Error('overdrawn');

  // happy path
  doThings(user);
}

Easier to read than nested if blocks.

2. The Switch Statement

switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  case 'RESET':
  case 'CLEAR':                  // fallthrough — multiple cases share body
    return 0;
  default:
    return state;
}

Comparison is strict equality (===). No type coercion.

Gotchas:

  • Missing break (or return) causes fallthrough — sometimes wanted, often a bug. ESLint catches accidental fallthrough.
  • case clauses share one scope. Declare let/const inside { } to scope per case:
case 'EDIT': {
  const { id } = action.payload;       // scoped to this block
  return updateById(state, id);
}
  • default doesn't have to be last, but conventionally is.

When you'd reach for switch but you really want a lookup:

const handlers = {
  INCREMENT: (state) => state + 1,
  DECREMENT: (state) => state - 1,
  RESET:     ()      => 0,
};

const handler = handlers[action.type];
return handler ? handler(state) : state;

3. Loops

3.1 for

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

Three semicolon-separated parts: init, condition, update. Any part can be empty: for (;;) is an infinite loop.

3.2 while / do…while

while (cond) {
  // runs zero or more times
}

do {
  // runs once, then while cond
} while (cond);

do…while rare — most loops want condition-first. Use when you genuinely need at-least-once semantics (read until end-of-stream, etc.).

3.3 for…of (iterables)

const arr = [1, 2, 3];
for (const x of arr) {
  console.log(x);
}

for (const ch of 'hello') {
  console.log(ch);
}

for (const [key, value] of map) {
  // Map yields [k, v] pairs
}

for (const [i, v] of arr.entries()) {
  // index + value
}

Works on anything iterable: arrays, strings, Map, Set, NodeList, arguments, generators, anything with Symbol.iterator. Use this for almost all "iterate values" loops.

3.4 for…in (object keys — careful)

const obj = { a: 1, b: 2 };
for (const key in obj) {
  console.log(key, obj[key]);
}
  • Iterates enumerable string keys, including inherited ones from the prototype.
  • Order is roughly insertion order for string keys, integer-like keys first.
  • Do not use on arrays — picks up custom properties and may return indices as strings ('0', '1').
  • Prefer Object.keys(obj), Object.entries(obj), or a for…of over them.
for (const [key, value] of Object.entries(obj)) {
  // ...
}

3.5 for await…of

Iterate async iterables (streams, paged APIs):

for await (const chunk of response.body) {
  process(chunk);
}

Requires async context (async function or top-level await in modules).

4. Iterating Collections

Want Recommended
Each value in an array for (const v of arr) or arr.forEach(v => …)
Index + value for (const [i, v] of arr.entries()) or classic for
Each key/value in an object for (const [k, v] of Object.entries(obj))
Each char in a string for (const ch of str) — handles surrogate pairs
Map / Set entries for (const x of collection)
Async stream for await (const x of stream)

forEach vs for…of:

  • for…of supports break, continue, return, await.
  • forEach doesn't — you can't break out, and async callbacks don't wait.
  • forEach is fine for fire-and-forget callbacks; for…of for everything else.

5. Break, Continue, Labels

for (let i = 0; i < 10; i++) {
  if (i === 5) break;        // exit loop entirely
  if (i % 2 === 0) continue; // skip rest of this iteration
  console.log(i);             // 1, 3
}

Labels name an outer loop so an inner break / continue can target it:

outer: for (let i = 0; i < grid.length; i++) {
  for (let j = 0; j < grid[i].length; j++) {
    if (grid[i][j] === target) {
      console.log('found at', i, j);
      break outer;            // exits both loops
    }
  }
}

Labels are rare in idiomatic code — refactoring to a function and return-ing is usually cleaner.

6. Array Iteration Methods

Higher-order functions; concise and chainable.

Method Returns Use when
map(fn) new array of fn(x) Transform each element.
filter(fn) new array of elements where fn(x) is truthy Pick a subset.
reduce(fn, initial) accumulator Fold into a single value.
forEach(fn) undefined Side effects, no break support.
find(fn) first matching element or undefined Look up one.
findIndex(fn) index or -1 Position of first match.
findLast(fn) / findLastIndex(fn) last match ES2023.
some(fn) boolean — any match? Early exits at first match.
every(fn) boolean — all match? Early exits at first non-match.
includes(value) boolean Strict-equality membership.
indexOf(value) index or -1 Strict equality.
flat(depth) flattened array Unnest nested arrays.
flatMap(fn) flattened map results One pass of map + flat(1).
at(i) element at index (negative OK) arr.at(-1) for last.
toSorted / toReversed / toSpliced new array (non-mutating) ES2023; replaces in-place sort/reverse.
const sum = nums.reduce((acc, n) => acc + n, 0);

const adults = people.filter(p => p.age >= 18).map(p => p.name);

const totalAge = people.reduce((acc, p) => acc + p.age, 0);
const averageAge = totalAge / people.length;

In-place vs returning new copy:

Mutating Non-mutating
sort, reverse, splice, push, pop, shift, unshift, copyWithin, fill toSorted, toReversed, toSpliced, slice, concat, with, flat, flatMap, map, filter, …

Prefer non-mutating in functional / Redux-style code.

7. Iterators and Generators

7.1 The Iterator Protocol

An iterator implements:

{
  next(): { value: any, done: boolean }
}

An iterable has Symbol.iterator returning an iterator. Arrays, strings, Maps, Sets, generators all implement it. for…of, spread, destructuring all consume it.

Manually:

const it = arr[Symbol.iterator]();
it.next();   // { value: 1, done: false }
it.next();   // { value: 2, done: false }
// ...
it.next();   // { value: undefined, done: true }

7.2 Generators

function* and yield build lazy iterators:

function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) yield i;
}

for (const n of range(0, 10, 2)) {
  console.log(n);    // 0, 2, 4, 6, 8
}

[...range(1, 4)];     // [1, 2, 3]

Each yield pauses execution; next() resumes. Useful for:

  • Infinite sequences without allocating arrays.
  • Streaming computation.
  • Building custom iterables for for…of.

Async generators (async function* + yield) + for await…of for streams:

async function* lines(url) {
  const resp = await fetch(url);
  for await (const chunk of resp.body) {
    yield* chunk.toString().split('\n');
  }
}

7.3 Iterator Helpers (ES2025)

map/filter/take/drop directly on iterators — lazy, no intermediate arrays:

const evens = naturalNumbers()
  .filter(n => n % 2 === 0)
  .take(5)
  .toArray();              // [0, 2, 4, 6, 8]

Solid Chromium/Firefox support since 2024; Safari 2025.

8. Throwing and Catching

try {
  doRisky();
} catch (err) {
  console.error(err.message);
} finally {
  cleanup();
}
  • catch binding is optional since ES2019: try { … } catch { … }.
  • finally runs on success, on caught exception, and when the try/catch returns. The finally block can override the return value if it returns.
  • Throw anything, but throw Error instances (or subclasses) — they carry a stack trace.
throw new Error('Bad input');
throw new TypeError(`expected number, got ${typeof x}`);

// Custom error classes — pattern-match in catch
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

try {
  validate(input);
} catch (err) {
  if (err instanceof ValidationError) {
    // handle
  } else {
    throw err;    // re-throw what you don't handle
  }
}

Error.cause (ES2022) chains causes:

try {
  load();
} catch (err) {
  throw new Error('Failed to initialise', { cause: err });
}

Async errors flow through await:

try {
  const data = await fetchData();
} catch (err) {
  // catches both sync errors before await and rejected promise after
}

9. References