# Error Handling in Asynchronous Code

## 1. try/catch Works Naturally with `await`

```js
async function loadUser(id) {
  try {
    const res = await fetch(`/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    console.error("Failed to load user:", e.message);
    throw e; // re-throw so the caller also knows it failed
  }
}
```

A rejected `await`ed Promise behaves exactly like a synchronous `throw` — this is the core reason `async`/`await` reads more naturally than Promise chains for error-heavy code.

## 2. `.catch()` Is the Promise-Chain Equivalent

```js
// Promise-chain style
fetchUser(id)
  .then(processUser)
  .catch(e => console.error("failed:", e.message));

// async/await style — behaviorally equivalent
async function run() {
  try {
    const user = await fetchUser(id);
    processUser(user);
  } catch (e) {
    console.error("failed:", e.message);
  }
}
```

## 3. Catching Errors from Multiple Awaited Steps at Once

```js
async function loadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const orders = await fetchOrders(user.id);
    const shipping = await fetchShipping(orders[0].id);
    return { user, orders, shipping };
  } catch (e) {
    // catches a failure from ANY of the three awaits above — the FIRST
    // one to fail short-circuits the rest, exactly like a Promise chain
    console.error("dashboard load failed at some step:", e.message);
    throw e;
  }
}
```

## 4. Unhandled Rejections

If a rejected Promise is never caught — no `.catch()`, no surrounding `try/catch` around its `await` — it becomes an **unhandled rejection**. Node.js logs a warning (and, depending on version/config, can crash the process); browsers log to the console and fire a `window.onunhandledrejection` event.

```js
async function riskyOperation() {
  throw new Error("nobody is watching for this");
}
riskyOperation(); // fire-and-forget, no await, no .catch() — unhandled rejection!

riskyOperation().catch(e => console.error("caught:", e.message)); // fixed
```

**Rule: never call an async function without either `await`ing it inside a `try/catch`, or attaching a `.catch()`.**

## 5. `Promise.all` and Error Handling

```js
async function loadAll() {
  try {
    const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
    return { a, b, c };
  } catch (e) {
    // fires as soon as ANY of the three rejects — the other two may
    // still be running in the background, but their results are lost here
    console.error("one of the parallel fetches failed:", e.message);
  }
}
```

If you need to know about **every** failure individually rather than just the first, use `Promise.allSettled` instead (see the Promises topic).

## 6. Cleanup with `finally` in Async Code

```js
async function loadWithSpinner() {
  showSpinner();
  try {
    return await fetchData();
  } catch (e) {
    showErrorMessage(e.message);
    throw e;
  } finally {
    hideSpinner(); // runs whether fetchData succeeded, failed, or even if the catch block re-threw
  }
}
```

`finally` guarantees cleanup logic (hiding a loading state, releasing a lock, closing a connection) runs regardless of outcome — the same guarantee it provides for synchronous code.

## Under the Hood: Why the "Unhandled Rejection" Warning Isn't Instant

A rejected Promise with no `.catch()` attached doesn't get flagged as "unhandled" the very instant it rejects — engines deliberately wait, because a `.catch()` might still be attached moments later, in the same synchronous turn or a very next one, and shouldn't be flagged as a problem if so.

![Between a rejection and the next microtask checkpoint, a late-attached .catch() still counts as handled](diagrams/01-grace-period.svg)

### The reject/handle tracking mechanism

The spec defines this via a host-level tracking hook (`HostPromiseRejectionTracker`, with `"reject"` and `"handle"` operations). When a Promise rejects with no reaction attached yet, the engine records it as **potentially unhandled** and schedules a check — rather than reporting immediately. If a `.catch()` (or a rejection-handling `.then()`) gets attached before that check runs, the tracker records a matching `"handle"` operation, and no warning is ever surfaced.

```js
const p = Promise.reject(new Error("boom")); // marked "potentially unhandled" internally

setTimeout(() => {
  p.catch(e => console.log("handled late:", e.message)); // attached within the SAME turn/soon after — no warning fires
}, 0);
```

### When the check actually fires

Engines perform this check at the end of a microtask checkpoint (V8: after the microtask queue is fully drained — see the Microtasks vs Macrotasks topic's checkpoint discussion) — if, at that point, a rejected Promise still has no reaction attached anywhere, only *then* does Node emit its `unhandledRejection` warning (or a browser fire `window.onunhandledrejection`). This is exactly why the warning can feel like it fires "a tick late" relative to where the rejection actually happened in your code — it's not a delay bug, it's the engine deliberately giving late-attached handlers a real window to attach before judging the rejection unhandled.

### Why "fire and forget" is genuinely risky, precisely because of this window

```js
async function riskyOperation() { throw new Error("nobody is watching"); }
riskyOperation(); // rejects; the tracker waits for a handler that will NEVER come — genuinely unhandled
```

Because nothing in this code path ever attaches a `.catch()` at all — not late, not ever — the tracker's grace period expires with no matching `"handle"` operation recorded, and the warning fires for real. The mechanism isn't lenient about *whether* you handle a rejection, only about *exactly when* within the same logical turn you're allowed to do it.

## 7. Common Pitfalls

1. **Forgetting `await` before a function call inside `try`** — without `await`, a rejected Promise won't be caught by the surrounding `try/catch`; the `catch` block only intercepts synchronous throws and *awaited* rejections.
2. **"Fire and forget" async calls with no error handling** — the #1 source of unhandled rejection warnings/crashes.
3. **Swallowing errors silently in `catch`** — logging nothing and not re-throwing hides real failures from callers who need to know.
4. **Catching too broadly around multiple unrelated awaits** — makes it unclear which specific step failed; consider per-step try/catch when the distinction matters.
5. **Not using `finally` for cleanup**, instead duplicating cleanup code in both the success path and every catch branch.

## 8. Quick Reference

```
try { await risky() } catch (e) { handle(e) } finally { cleanup() }
async fn() without await/.catch()   → unhandled rejection risk
Promise.all + try/catch              → fails fast on first rejection
Promise.allSettled                    → no rejection, per-item status instead
```
