---
title: "09 — Control Flow and Errors — Branching, Throwing, and Catching"
uid: control-flow-and-errors
tags: ["switch", "roadmap:javascript", "errors", "try-catch", "if-else", "control-flow", "throw", "javascript"]
excerpt: "Branching picks a path; exceptions unwind the call stack. Two distinct flow mechanisms, and code gets readable when each is used for its actual job."
date: 2026-08-13T03:28:06+0000
source: https://www.aveshina.my.id/en/blog/control-flow-and-errors
---

"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](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](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](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](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](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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error)

```quiz
Q: What's the key difference between branching (if/switch) and exceptions (throw/try/catch)?
- Branching picks an expected path; exceptions unwind the call stack for unexpected failure
- They are two syntaxes for the same thing
correct: 0
explain: Branching handles expected alternatives inline. Exceptions bail out of the normal flow and propagate up the call stack until a catch handles them. Using exceptions for normal flow obscures the happy path and is a code smell.

Q: What does the finally block do?
- Runs cleanup code regardless of whether try threw or catch ran — even if try or catch returns
- Runs only if no exception was thrown
correct: 0
explain: finally always executes after try (and catch if present), even if try or catch contains a return or another throw. That's why it's the right place for cleanup like closing files or releasing locks.

Q: Why throw `new Error("msg")` instead of `throw "msg"`?
- An Error object preserves the stack trace; a string loses it
- There is no difference
correct: 0
explain: Error (and its subclasses) capture a stack trace, which is essential for debugging. A bare string or object has no stack, so the error becomes much harder to trace back to its source.

Q: In a switch, what happens if a case body omits break or return?
- Execution falls through into the next case's body
- It throws a SyntaxError
correct: 0
explain: Without break or return, switch falls through to the next case. This is occasionally intentional (grouping cases) but usually a bug — end each case with break or return.

Q: A catch block that handles every error type and returns a default value is risky because…
- it silently swallows errors you didn't intend to handle, hiding real bugs
- it's slower than letting the error propagate
correct: 0
explain: A catch-all that returns a default masks failures (including programming bugs) that should surface. Catch only the specific error types you can handle; re-throw the rest.
```
