09 — Control Flow and Errors — Branching, Throwing, and Catching
"If statements and try/catch" was my control-flow summary, and it fused two mechanisms with different jobs. The idea that everything else hangs off: JavaScript has two distinct flow mechanisms — branching (picking a path) and exceptions (unwinding the call stack) — and code gets readable when each is used for its actual job, not as a substitute for the other. [1]
The framing that finally landed is separating the two mechanisms by intent. Branching (if/else, switch) is for expected alternatives — the request succeeded or failed, the user is admin or not. Exceptions (throw/try/catch) are for unexpected failure — something broke the normal path and I need to bail out to a handler, possibly several frames up. Using exceptions for normal control flow (throwing to break out of a loop, catching to handle a known condition) is a smell; using if checks for genuinely exceptional failure buries the happy path in guards.
Branching: if/else and switch
The basic conditional is if, optional else if, optional else [2]. For a single condition or two paths, it's always the right choice. When one value is being compared against many discrete options, switch reads cleaner [3]:
switch (status) {
case "loading": return <Spinner />;
case "error": return <Error />;
case "success": return <Data />;
default: return null;
}The trap with switch is fall-through: without a break (or return), execution falls into the next case. Sometimes that's intentional (grouping cases), often it's a bug. Modern style is to end every case with return or break and reserve fall-through for deliberate grouping with a comment.
Exceptions: throw, try/catch/finally
Throwing is how a function signals "I can't continue, deal with it." Execution of the current function stops and control passes up the call stack to the nearest enclosing catch [4][5]:
try {
const data = JSON.parse(input); // may throw SyntaxError
return transform(data);
} catch (err) {
console.error("Bad input:", err.message);
return null;
} finally {
cleanup(); // always runs, throw or not
}The three blocks have distinct roles: try wraps the risky code, catch receives the thrown value (conventionally an Error), finally runs cleanup regardless of outcome — closing files, releasing locks, resetting state. finally runs even if try or catch returns, which is the subtle part worth remembering.
Error objects and their types
JavaScript provides a set of built-in error subclasses, each signalling a different failure mode [6]:
- Error — the base. new Error("message").
- TypeError — wrong type (undefined.foo).
- RangeError — value out of range (infinite recursion, bad array length).
- SyntaxError — invalid syntax, often from JSON.parse or eval.
- ReferenceError — referencing an undeclared variable.
Each carries a message and a stack trace (non-standard but universally implemented). Catching by type lets me handle different failures separately:
try { /* … */ }
catch (err) {
if (err instanceof SyntaxError) { /* bad JSON */ }
else if (err instanceof TypeError) { /* wrong shape */ }
else { throw err; } // re-throw what I don't handle
}The re-throw pattern matters: a catch that swallows every error silently hides bugs. Catch what you can actually handle; let the rest propagate.
Always throw an Error object, not a string. throw "oops" loses the stack trace; throw new Error("oops") keeps it. The stack is what makes the error debuggable.
throw is for exceptional flow only
The discipline I keep: throw for exceptional conditions only — "the input was supposed to be valid but isn't," "the network call failed." For expected alternatives (a search returned no results, a user isn't authorized), return a sentinel or a result object — don't throw. Exceptions are expensive (they unwind the stack) and they obscure the happy path; using them for normal flow turns every call site into a try/catch.
How I use this
The practical split: if/else and switch for expected paths; try/catch for genuinely exceptional failure, scoped as tightly as possible. I keep catch blocks small — wrap just the line that can fail, not the whole function — and re-throw anything I can't handle, so a swallowed error never hides a real bug. And every throw is new Error(...) or a subclass, never a bare string, so the stack trace survives to the log.
References
[1] Mozilla, "Control flow and error handling," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling
[2] I. Kantor, "Conditional branching: if, ?," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/ifelse
[3] Mozilla, "switch statement," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch
[4] Mozilla, "try...catch statement," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
[5] Rollbar, "Throwing exceptions in JavaScript," 2022. [Online]. Available: https://rollbar.com/guides/javascript/how-to-throw-exceptions-in-javascript
[6] Mozilla, "Error object," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
Knowledge check · Question 1 of 5
What's the key difference between branching (if/switch) and exceptions (throw/try/catch)?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!