Control flow

Write branches and loops that stay flat and readable.

07of 32
≈ 10Min read
8Parts

if, and getting out early

Handle the cases you are rejecting first and return. What is left is the main path, at one level of indentation, and it stays that way when the next condition is added.

the same logic, two shapes
// nested: the real work is four levels in
function price(door) {
  if (door) {
    if (door.days) {
      if (door.days > 0) {
        return door.days * 100;
      }
    }
  }
  return 0;
}

// early return: the exceptional cases first, the real work unindented
function price(door) {
  if (!door) return 0;
  if (!door.days || door.days <= 0) return 0;
  return door.days * 100;
}

Braces

one exception
// fine: a guard on one line
if (!user) return;

// everything else gets braces
if (ready) {
  start();
}

// this is how a bug gets in
if (ready)
  start();
  log();      // runs ALWAYS — the indentation lies

switch

compares with ===, and falls through
switch (kind) {
  case "painted":
  case "veneer":            // deliberate fall-through: both do the same
    return 5;
  case "ceramic":
    return 15;
  default:
    return 10;
}

// with braces when a case declares something
switch (kind) {
  case "painted": {
    const base = 5;
    return base;
  }
}
Often a lookup is clearer

For a plain value-to-value mapping, an object beats a switch: const days = { painted: 5, ceramic: 15 }[kind] ?? 10;

Loops

four kinds, and when each is right
// values of anything iterable — the default
for (const d of doors) { }

// with the index
for (const [i, d] of doors.entries()) { }

// a counted loop, when you need the index arithmetic
for (let i = 0; i < doors.length; i++) { }

// until a condition changes
while (queue.length) { }

// keys of an object — but prefer Object.keys/entries
for (const k in obj) { }
for…in walks the prototype chain too

It visits inherited enumerable properties, so it can surprise you on anything but a plain literal. Object.keys(obj) gives you only the object's own keys.

break, continue, and labels

and the one place a label earns its keep
for (const d of doors) {
  if (d.archived) continue;    // skip this one
  if (d.days > 30) break;      // stop entirely
  use(d);
}

outer:
for (const row of rows) {
  for (const cell of row) {
    if (cell === target) break outer;   // leaves BOTH loops
  }
}

Loop or array method?

the one that silently does nothing
// WRONG: forEach does not wait, so this finishes instantly
doors.forEach(async (d) => { await save(d); });

// in turn
for (const d of doors) { await save(d); }

// all at once
await Promise.all(doors.map((d) => save(d)));
You wantUse
A new array of the same lengthmap
A subsetfilter
One value from manyreduce
The first matchfind
A yes/no answersome / every
To stop part-waya loop — map cannot break
To await each item in turna loop — forEach ignores promises
A side effect per itema loop, or forEach if it is short

Truthiness in conditions

say what you mean
if (list)           // true even for an empty array
if (list.length)    // "has items"

if (count)          // false when count is 0 — probably a bug
if (count != null)  // "a value was provided"

if (str)            // false for "" — sometimes right, sometimes not
if (str.trim())     // "has visible text"

Ranges

JavaScript has no a < x < b
if (0 < x < 10) { }        // always true — (0 < x) is a boolean, then boolean < 10
if (x > 0 && x < 10) { }   // what you meant

const between = (x, lo, hi) => x >= lo && x <= hi;

Practice