Chapter 3 · Foundations Revisited

Control Flow, Errors & Loops

Branching, exceptions and loops look familiar, but each has sharp edges: falsy values, switch fall-through, finally overriding return, and the crucial difference between for…in and for…of.

You will learn

  • The eight falsy values, and if/switch pitfalls.
  • throw, try/catch/finally, the optional catch binding, and error cause.
  • The built-in error types, and how to write your own error class.
  • Every loop: for, while, do…while, labels, break/continue.
  • for…in (keys) vs for…of (values) ES2015, and when to use which.

Conditions and truthiness

An if condition doesn't need a boolean: JavaScript converts the value. Exactly eight values are falsy, and everything else is truthy, including "0", "false", [] and {}.

Falsy values
false · 0 · -0 · 0n · "" · null · undefined · NaN

⚠️ Pitfall: if (qty) treats a real quantity of 0 as "missing". When 0 or "" are valid values, test explicitly: if (qty !== undefined), or use ?? (Chapter 5).

switch compares with ===, and falls through

switch compares with strict equality (===), and execution falls through into the next case unless you break or return. Wrap a case body in { } if you declare let/const inside it, because all cases share one block scope.

Exceptions

throw can throw any value, but always throw an Error (or a subclass): it records a stack trace, and the code that catches it can rely on .name and .message.

The built-in error types

TypeTypical cause
TypeErrorusing a value the wrong way: undefined.x, calling a non-function, writing to a const
ReferenceErroran undeclared name, or the TDZ
RangeErrora number out of range: new Array(-1), (1).toFixed(200), infinite recursion
SyntaxErrorinvalid code, or JSON.parse("{bad}")
URIError, EvalErrorbad decodeURIComponent input / legacy
AggregateError ES2021several errors at once, e.g. from Promise.any (Chapter 12)

Optional catch binding ES2019 and error cause ES2022

Custom error classes

Extend Error (classes are covered in Chapter 11). Set name, so that logs show your type, and add any extra data as properties.

finally can override return

❌ return in finally swallows everything

✅ Use finally only for clean-up

Loops

LoopUse it for
for (let i = 0; i < n; i++)counting, or when you need the index arithmetic
while (cond)repeat while a condition holds
do { … } while (cond)run at least once
for (const v of iterable) ES2015the values of arrays, strings, Maps, Sets and any iterable
for (const k in object)the enumerable string keys of an object (inherited ones too)
array.forEach(fn)side effects per item; you can't break out of it

for…of vs for…in

❌ for…in on an array

✅ for…of for values

☕ Java: for (const x of list) is Java's enhanced for loop, for (String x : list). JavaScript's for…in has no Java equivalent; think of it as "for each key of an object".

break, continue and labels

A label names a loop, so that break or continue can target an outer loop. It's rare, but it's the clean way to leave nested loops.

do…while

More examples

1. Validating an order with early returns

2. Retry with a counter and a final error

3. Counting categories with for…of

4. Mapping status codes with switch(true)

5. Guarding JSON input

Recap

  • Eight values are falsy. Test explicitly when 0 or "" are valid.
  • switch uses === and falls through without break/return.
  • Throw Error objects. catch {} needs no binding, wrap errors with { cause }, and never return from finally.
  • Extend Error for domain errors, and re-throw what you don't handle.
  • for…of gives values, for…in gives string keys (even inherited ones). Use entries() for indexes, and labels to break out of nested loops.

Exercises

Exercise 1 🟢 easy predict the output

Which of these are truthy? "0", 0n, [], -0, "false", NaN

Show solution

Truthy: "0", [], "false". Falsy: 0n, -0, NaN. Non-empty strings and all objects (even empty ones) are truthy.

Exercise 2 🟢 easy fix the bug

Every size prints large. Fix it.

Show solution

Without break or return, execution falls through every following case, so default always ran last.

Exercise 3 🟢 easy refactor it

Rewrite with for…of and entries(): for (var i = 0; i < titles.length; i++) console.log(i + 1 + ". " + titles[i]);

Show solution

entries() yields [index, value] pairs, which destructuring splits apart.

Exercise 4 🟢 easy explain why

Why should you throw new Error("x") rather than throw "x"?

Show solution

An Error carries a stack trace, plus name, message and optionally cause. Code that catches it can use instanceof and those properties. A thrown string has none of that, and is hard to debug.

Exercise 5 🟡 medium predict the output

What does this print?

Show solution

try, finally, from try. The return value is computed, then finally runs, and then the function actually returns.

Exercise 6 🟡 medium build it

Write firstOver(matrix, limit), which returns the [row, col] of the first number over limit, or null. Use a labelled break.

Show solution

break outer exits both loops at once. A return inside the loop would work too, and is often simpler.

Exercise 7 🟡 medium build it · playground

Create playground/errors.js exporting ValidationError (with a field property) and validateBook(book), which throws for a missing title or a non-positive integer price, and wraps any error from JSON.parse in loadBook(json) using cause.

Show solution

Passing options through to super(message, options) lets callers attach a cause.

Exercise 8 🟡 medium predict the output

What does this print?

Show solution

[ 'own', 'inherited' ] [ 'own' ]. for…in also walks enumerable inherited keys, while Object.keys returns only the object's own keys.

Exercise 9 🔴 hard explain why

Why can't you stop array.forEach with break? Give two alternatives for "stop at the first match".

Show solution

The forEach callback is a separate function; break is only valid inside a loop statement, and return just ends that one callback call. Alternatives: a for…of loop with break, or methods that stop early by design: find, findIndex, some and every (Chapter 8).

Exercise 10 🔴 hard mini-project · playground

Create playground/checkout.js. Given a stock object and an order (a list of { id, qty }), collect every problem (an unknown book, an invalid qty, not enough stock) instead of stopping at the first. If there are any problems, throw one AggregateError. Otherwise reduce the stock and return the new stock. Print the outcome for one good order and one bad order.

Show solution

It combines loops with continue, the in operator, built-in error types, AggregateError, and a non-mutating update with spread.

Official docs