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/switchpitfalls. throw,try/catch/finally, the optional catch binding, and errorcause.- 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) vsfor…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
| Type | Typical cause |
|---|---|
TypeError | using a value the wrong way: undefined.x, calling a non-function, writing to a const |
ReferenceError | an undeclared name, or the TDZ |
RangeError | a number out of range: new Array(-1), (1).toFixed(200), infinite recursion |
SyntaxError | invalid code, or JSON.parse("{bad}") |
URIError, EvalError | bad decodeURIComponent input / legacy |
AggregateError ES2021 | several 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
| Loop | Use 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) ES2015 | the 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
0or""are valid. switchuses===and falls through withoutbreak/return.- Throw
Errorobjects.catch {}needs no binding, wrap errors with{ cause }, and neverreturnfromfinally. - Extend
Errorfor domain errors, and re-throw what you don't handle. for…ofgives values,for…ingives string keys (even inherited ones). Useentries()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.