# Error Handling

## 1. try / catch / finally

```js
try {
  JSON.parse("{ invalid json");
} catch (error) {
  console.log("Caught:", error.message);
} finally {
  console.log("Runs regardless of success or failure");
}
```

`finally` always runs — even if `catch` itself throws, or if `try` contains a `return` statement (the `finally` block runs before the function actually returns).

![A thrown error unwinds from frame D up through frame C, stopping at frame B's try/catch handler](diagrams/01-stack-unwinding.svg)

## 2. The Built-in Error Types

| Type | Thrown when |
|---|---|
| `Error` | generic base class — all others extend it |
| `TypeError` | operation on a value of the wrong type (`null.foo`, calling a non-function) |
| `RangeError` | numeric value outside allowed range (`new Array(-1)`) |
| `ReferenceError` | referencing an undeclared variable, or a `let`/`const` in its TDZ |
| `SyntaxError` | malformed code, usually caught by `JSON.parse` on bad input |

```js
try {
  null.foo;
} catch (e) {
  console.log(e instanceof TypeError); // true
  console.log(e.name);                  // "TypeError"
  console.log(e.message);               // "Cannot read properties of null (reading 'foo')"
}
```

## 3. Custom Error Classes

```js
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = "ValidationError"; // otherwise inherits "Error" as the name
    this.field = field;
  }
}

function validateAge(age) {
  if (age < 0) throw new ValidationError("Age cannot be negative", "age");
}

try {
  validateAge(-5);
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`Invalid field "${e.field}": ${e.message}`);
  } else {
    throw e; // re-throw anything you don't specifically handle
  }
}
```

Custom error classes let `catch` blocks discriminate between different failure types using `instanceof`, instead of parsing error message strings.

## 4. Re-throwing and Catch-and-Rethrow

```js
function processData(data) {
  try {
    return JSON.parse(data);
  } catch (e) {
    throw new Error(`Failed to process data: ${e.message}`, { cause: e }); // wrap with context, keep the original via `cause`
  }
}
```

`{ cause: e }` (ES2022) preserves the original error for debugging while adding higher-level context — check `error.cause` to trace the root failure.

## 5. Error Handling in Async Code (Preview)

```js
async function loadUser(id) {
  try {
    const response = await fetch(`/users/${id}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (e) {
    console.error("Failed to load user:", e.message);
    throw e;
  }
}
```

`try/catch` works around `await` exactly like synchronous code (full detail in the Async Error Handling topic) — a rejected Promise is caught the same way a thrown error is.

## 6. What `try/catch` Does NOT Catch

```js
try {
  setTimeout(() => { throw new Error("boom"); }, 0);
} catch (e) {
  console.log("never runs"); // the callback executes LATER, outside this try block entirely
}
```

`try/catch` only guards the **synchronous** code directly inside it. Errors thrown inside callbacks, `setTimeout`, or unrelated async work must be caught **inside those callbacks themselves**, or via `.catch()` on the resulting Promise.

## Under the Hood: Stack Unwinding and Stack Trace Capture

### `throw` triggers a real stack-unwinding walk

When `throw` executes, the engine doesn't "search" for a handler in the abstract — it physically walks back up the **call stack** (see the Call Stack topic), popping one frame at a time, checking at each level whether that frame is inside an active `try` block with a matching `catch`. Any `finally` block belonging to a frame being popped during this walk executes along the way, before the walk continues further up. If the walk reaches the very top of the stack with no handler found, the error becomes **uncaught** — in Node this crashes the process by default; in a browser it logs to the console and fires a `window.onerror` event.

```
throw new Error("boom")   at frame D
  ↓ unwind: is D in a try with a catch? no → pop D, run any finally in D
  ↓ unwind: is C in a try with a catch? no → pop C, run any finally in C
  ↓ unwind: is B in a try with a catch? YES → stop here, run B's catch block
     (A, further up, is never even inspected)
```

### `.stack` is captured lazily, and it isn't free

Constructing an `Error` doesn't necessarily do the (relatively expensive) work of walking the call stack and formatting it into the `.stack` string immediately — V8 defers this: the raw stack frame information is captured cheaply at `throw`/construction time, but formatting it into the readable string you see only happens the first time something actually **reads** `.stack`. This is why creating errors in a tight loop "just in case" (without ever reading `.stack`) is cheaper than it looks, but reading `.stack` on every one of them is not. V8 also caps how many frames get captured at all via an internal `Error.stackTraceLimit` (10 by default) — a deeply nested call chain's stack trace gets truncated, not because the engine forgot the rest, but because capturing unlimited depth on every thrown error would be wasteful for the overwhelming majority of errors that are never inspected that deeply.

### `try/catch` used to block optimization — it no longer does

For years, older JavaScript engines could not apply their optimizing compiler (see the Functions topic's tiered pipeline) to any function containing a `try/catch` block at all — the function was permanently stuck running in the slower interpreted tier. Modern engines (V8's TurboFan and equivalents in other engines) removed this restriction entirely; `try/catch` no longer prevents a function from being optimized. This historical fact still circulates as outdated advice ("avoid try/catch in hot loops for performance") — worth knowing it was true once, and is no longer a real concern on current engines.

## 7. Common Pitfalls

1. **Swallowing errors silently** — an empty `catch {}` block hides bugs; at minimum log the error, and re-throw when the caller needs to know.
2. **Catching too broadly** — wrapping large blocks of unrelated code in one `try` makes it unclear which line actually failed; keep `try` blocks focused.
3. **Forgetting `finally` runs even after `return`** — a `return` inside `try` is evaluated, but `finally` still executes before control actually leaves the function, and can even override the return value if `finally` itself contains a `return`.
4. **Throwing non-Error values** — `throw "failed"` works but loses the stack trace and `.message`/`.name` structure; always `throw new Error(...)` or a subclass.
5. **Expecting `try/catch` to catch async callback errors** — see section 6; this is one of the most common misunderstandings for developers new to JS.

## 8. Quick Reference

```
try { risky() } catch (e) { handle(e) } finally { cleanup() }
e instanceof TypeError / RangeError / ReferenceError / YourCustomError
class MyError extends Error { constructor(msg) { super(msg); this.name = "MyError"; } }
throw new Error("msg", { cause: originalError })
```
