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.
// 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
// 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
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
// 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
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?
// 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 want | Use |
|---|---|
| A new array of the same length | map |
| A subset | filter |
| One value from many | reduce |
| The first match | find |
| A yes/no answer | some / every |
| To stop part-way | a loop — map cannot break |
To await each item in turn | a loop — forEach ignores promises |
| A side effect per item | a loop, or forEach if it is short |
Truthiness in conditions
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
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;