---
name: javascript-error-handling
description: "Audits and fixes JavaScript error handling: empty or log-only catch blocks, rethrows that lose the cause, fetch calls that treat 404 and 500 as success, JSON.stringify on errors, Node processes that survive uncaughtException, React error boundary gaps, and 'Script error.' reports. Runs a scanner, then edits flagged lines only. Use for 'swallowed error', 'unhandled rejection', 'error.cause', 'error boundary not working'. Do NOT use for promise combinators, cancellation, retry policy or error-reporting vendor setup."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/javascript/javascript-error-handling/
  category: frontend-development
  subcategory: javascript
  version: "1.0.0"
  created: "2026-11-07"
  updated: "2026-11-07"
---

# JavaScript Error Handling: Decide Where Each Failure Is Handled

An agent works on error handling in this order: list every place a failure can happen, decide which layer can act on it, run the scanner, fix catch blocks first, then HTTP responses, then error classes and causes, then logging, then the last-resort global handlers, then the user-facing boundary, and re-run the scanner and a forced-failure test. The scanner reads text. It cannot tell whether a layer can recover, so it reports patterns for a person or agent to confirm.

Reusable code is in `references/patterns.md`. Availability notes and the rival comparison are in `references/appendix.md`.

## When to use this skill, and when not to

Use it when errors vanish (empty or log-only `catch`), a rethrown error has lost its original stack, a 404 or 500 is treated as success, `JSON.stringify(error)` logs `{}`, a Node process keeps running after `uncaughtException`, a React error boundary "does not work", or an error reporter shows `Script error.` with no detail.

| Request | Use instead |
|---|---|
| Floating promises, `forEach(async)`, choosing `all`, `allSettled`, `any` or `race` | `promises-async-await.md` |
| `AbortError`, `TimeoutError`, cancelling requests | `abortcontroller-cancellation.md` |
| Retry and backoff policy, caching, request state | A state and data fetching skill. Out of scope |
| Choosing or configuring an error-reporting vendor | Out of scope. Check the vendor's documentation |
| Server error handling beyond Node process-level hooks (Express, Nest) | Out of scope |

## Guardrails

1. **Propose, then change.** Report first. Edit only after the user approves a batch, on a branch under version control. Never push, merge or deploy.
2. **Never run the user's code against live services to test a fix.** Use the project's tests, mocks or a test environment.
3. **Data handled.** The scanner prints file, line, rule ID and a fixed message, never source text. Error messages, `cause` values, stacks and request URLs often contain user input, email addresses, ids, tokens or query strings. Do not paste them into prompts, reports or tickets. Redact before logging (`references/patterns.md`, serialiser). Use fake data in tests.
4. **Nothing is invented.** Every rule carries a source and a date. Version-dependent behaviour carries **verify before relying on this**. Retry counts, timeouts and log levels come from the user.
5. **Refuses to:** add an empty `catch` to silence a log, keep a Node process alive after `uncaughtException` to "stay up", send errors to a third-party reporter without the user naming it, put raw error objects, request bodies or headers into logs, or declare a fix verified without re-running the scanner and the forced-failure test.

## Inputs the agent must have before starting

Ask for any that are missing. Do not guess.

| Input | If missing, ask |
|---|---|
| Target files or directory | "Which folder or files should I scan? I will skip `node_modules`, `dist` and `build`." |
| Runtime | "Is this browser code, Node, or both? Which framework (React, Vue, plain)?" |
| Where errors should go | "Where do errors get reported today (console, a logger, a vendor)? I will not add a new one." |
| What the user sees on failure | "What should a person see when this fails: message, retry button, fallback content?" |
| Test command | "How do I run the tests? If none exist I will propose one forced-failure test per fix." |
| Node version (Node code only) | "Which Node version runs in production?" Default-mode behaviour changed between majors |

## Workflow

### Step 1: Decide who can act

For each failure site, apply this table. Read it once, then do not re-derive it per file.

| Failure | Class | Handle where |
|---|---|---|
| Network down, 4xx/5xx, missing file, invalid user input, timeout | Operational. Expected | The layer that can retry, fall back or tell the user. Elsewhere, wrap and propagate |
| `TypeError` from your own code, failed invariant, `undefined` where a value is required | Programmer error. A bug | Do not catch to recover. Let it reach the boundary and the global handler, report it, fix the code |
| Third-party library throws | Unknown | Catch at your adapter, wrap in your own error with `cause`, propagate |

Source: Node-focused guides use the operational versus programmer split and advise failing fast when recovery is not possible (site24x7 guide, read 2026-11-07).

If a layer cannot say what it would do with the error (retry, substitute, show text), it must not catch it.

### Step 2: Run the scanner

```bash
python3 scripts/errors_check.py <paths>
```

Read only its output. Do not read the script. Each line is `path:line: ID level message`. Exit code 1 means errors, or warnings with `--strict`.

| ID | Level | Meaning | Fix in step |
|---|---|---|---|
| E1 | error | Empty `catch` or `.catch(() => {})` | 3 |
| E2 | warn | `catch` only calls `console.*` | 3 |
| E3 | warn | `throw new X(...)` in a `catch` with no `cause` | 3, 5 |
| E4 | error | `throw "text"`, `throw {…}` or `throw 42` | 5 |
| E5 | warn | `fetch` in a file with no `.ok` or `.status` check | 4 |
| E6 | error | `uncaughtException` handler that does not exit | 7 |
| E7 | warn | `unhandledRejection` handler that neither exits nor reports | 7 |
| E8 | warn | `JSON.stringify(err)` | 6 |
| E9 | warn | Caught variable never used | 3 |
| E10 | warn | `catch (e) { throw e }` | 3 |
| E11 | warn | `preventDefault()` inside an `unhandledrejection` handler | 7 |
| P1 | info | No global error or rejection handler in the scanned files | 7 |
| P2 | info | React imported but no error boundary in the scanned files | 8 |

The scanner is text-based. E5 looks for `.ok` or `.status` anywhere in the same file, so a check in a different file looks like a miss. P1 and P2 look only at the scanned paths: scan the whole source tree or ignore them.

If the scanner reports more than 40 lines, fix errors first, then one rule ID at a time. Do not read every flagged file at once. Read only the lines reported, plus 10 lines either side.

### Step 3: Fix catch blocks (E1, E2, E9, E10)

For each flagged `catch`, apply the first row that fits:

| Situation | Edit |
|---|---|
| The layer can recover (retry, default value, fallback UI) | Keep the `catch`. Do the recovery. Report the error once (`logger`, not `console`) with a stable event name |
| The layer cannot recover | Delete the `catch`, or wrap with `{ cause }` and rethrow (step 5) |
| Cleanup is needed either way | Use `finally`. Do not catch just to clean up |
| `catch (e) { throw e }` | Delete it |
| Error is expected and harmless (a feature probe, `JSON.parse` of optional data) | Narrow the `try` to the single call, catch, add a comment naming the expected condition, and return the default |

Do not log and rethrow the same error at every layer. Report it once, where it is finally handled. Guides list "logging and re-throwing" as a pitfall (wshobson error-handling-patterns, read 2026-11-07).

### Step 4: Check HTTP responses (E5)

`fetch` resolves as soon as headers arrive. It rejects only when the request cannot be made. A 404 or 500 resolves, and `response.ok` is true only for 200 to 299 (javascript.info/fetch, read 2026-11-07).

For each flagged call:

1. Check `response.ok` before reading the body.
2. Throw a typed `HttpError` carrying `status` and `url` (template in `references/patterns.md`). Strip the query string from `url` before it is logged: query strings carry tokens and emails.
3. Read the body once. A second read fails (javascript.info/fetch). If you need the error body, read it as text in the failure branch and parse it yourself.
4. If a wrapper such as `axios` or `ky` already throws on non-2xx, do not add a second check. The scanner treats those names as handled. **Verify against the library version in use.**

### Step 5: Error classes and `cause`

Rules:

1. Throw `Error` or a subclass. Never a string or plain object (E4): they carry no stack and fail `instanceof Error`.
2. Create a subclass only when a caller will branch on it with `instanceof`. Otherwise use `Error` with a message and `cause`. Matching on `err.message` text breaks the first time someone edits the message.
3. Set `this.name` in the subclass. The parent constructor sets it to `"Error"` (javascript.info/custom-errors, read 2026-11-07).
4. When wrapping, pass `{ cause: err }`: `new Error("Could not save order", { cause: err })`. It is Baseline widely available since September 2021 (MDN `Error.cause`, read 2026-11-07).
5. `cause` can be any value. Check `instanceof Error` before treating it as one (MDN).
6. For several unrelated failures use `AggregateError`, not a `cause` array (MDN).
7. In `catch`, rethrow anything you do not recognise with `throw err` (javascript.info/custom-errors).
8. Keep user input out of messages that reach logs. Put identifiers in structured fields.

### Step 6: Log in a form that survives (E8)

`message`, `stack` and `cause` are not enumerable, so `JSON.stringify(new Error("x", { cause: 1 }))` returns `{}`. This was checked on Node 22 on 2026-11-07 and agrees with MDN's note that `cause` is non-enumerable. Use the serialiser in `references/patterns.md`: it walks the `cause` chain with a cycle guard and redacts query strings.

Log fields: event name, error name, message, stack (server side only), cause chain, request id. Do not log request bodies, headers, cookies or tokens.

### Step 7: Last-resort global handlers (E6, E7, E11, P1)

These are for reporting and shutdown. They are not for recovery.

| Runtime | Rule |
|---|---|
| Browser | Register `unhandledrejection` and `error` once, at startup. Report the reason, keep the console error visible. Calling `preventDefault()` stops the default console error (MDN, read 2026-11-07), so only do it after the report is sent. Rejections from cross-origin scripts do not fire the event (MDN) |
| Node `unhandledRejection` | Log the reason and exit non-zero, or let the default stand. `throw` mode is the default: it raises the rejection as an uncaught exception if no handler deals with it (Node process docs, read 2026-11-07). Node 15 introduced it as the default (**verify before relying on this**) |
| Node `uncaughtException` | Log, do synchronous cleanup, set `process.exitCode`, exit. The process state is undefined after one. A listener replaces the default exit, and the process ends with 0 unless you set `process.exitCode`. Restart with an external supervisor in a separate process (Node docs) |
| Node `uncaughtExceptionMonitor` | Observe-only. Use it for reporting when you do not want to change the crash behaviour (Node docs) |

If P1 fires on a front-end project with no handler, add the browser template. If it fires on a library, ignore it: libraries should not register global handlers.

**Cross-origin scripts.** If reports show `Script error.` with no line or message, the script is from another origin. Add `crossorigin="anonymous"` to the tag and send `Access-Control-Allow-Origin` from the server. Both are needed. Prefer a specific origin to `*` (Sentry answers, read 2026-11-07).

### Step 8: The user-facing boundary (P2)

React error boundaries are class components. They catch rendering errors in children. They do **not** catch errors in event handlers, async code such as `setTimeout`, server-side rendering, or errors thrown by the boundary itself. Errors thrown inside functions passed to `startTransition` are the exception and are caught (React reference, read 2026-11-07).

Rules:

1. Put one boundary at the app root and one around each independently failing region (a widget, a route). If a region has a safe fallback, give it its own boundary.
2. Event handlers and async callbacks need their own `try/catch` that sets error state. The `react-error-boundary` package offers `useErrorBoundary` for this: **check its current API before use**.
3. Every error UI needs three things: what failed in plain words, what the person can do (retry, go back), and the request id for support.
4. Boundaries reset on a key change or a retry action. A boundary that never resets leaves the region dead until a full reload.

Vue and Svelte have their own error hooks (`onErrorCaptured`, `<svelte:boundary>`): out of scope here, so use their documentation.

### Step 9: Verify and report

1. Re-run the scanner. Errors must be zero. Remaining warnings need a one-line justification each.
2. For each fix, run or write one forced-failure test: make the dependency throw or return 500, then assert (a) the caller sees the typed error, (b) `cause` is present, (c) one log line was written, (d) the UI shows the fallback.
3. Report with the template below. Do not echo unchanged code.

## Worked example

Input: `orders.js`, 38 lines, a browser and Node mix. Scanner run:

```
orders.js:4: E5 warn fetch without a response.ok or status check: HTTP 4xx/5xx resolves
orders.js:11: E9 warn caught error never used
orders.js:12: E3 warn rethrow without { cause }: original stack lost
orders.js:19: E1 error empty catch: handle, rethrow or report
orders.js:24: E4 error throws a non-Error: no stack, no instanceof
orders.js:27: E2 warn catch only logs: the caller never learns it failed
orders.js:33: E8 warn JSON.stringify on an Error gives {}: pick message, stack, cause
orders.js:36: E6 error uncaughtException handler does not exit: process state is undefined
scanned 1 files: 3 errors, 5 warnings
```

Decisions: `loadOrder` cannot recover, so it throws `HttpError` (E5). `saveOrder` cannot recover, so it wraps with `{ cause }` (E9 and E3 clear together). `parseCart` can recover with `[]`, so it keeps the `catch`, reports once and returns the default (E1). `validate` is a programmer-error check, so `throw new TypeError(...)` replaces the string and the `catch` that only logged is deleted (E4, E2). `report` uses the serialiser (E8). The process handler logs, sets `process.exitCode` and exits (E6).

Second run: `scanned 1 files: 0 errors, 0 warnings`, exit 0. The first run exits 1.

Result report (illustrative: the call sites and test counts are invented for the example):

```
Fixed 8 of 8 flagged lines in orders.js. Scanner: 3 errors, 5 warnings -> 0, 0.
Behaviour change: loadOrder now throws HttpError on non-2xx. Callers that
assumed a parsed body must catch it (2 call sites: cart.js:18, checkout.js:44).
Tests added: orders.test.js, 3 forced-failure cases. Not run: no test command given.
```

## Templates

### Report

```
Scope: <paths>, <n> files, <runtime>
Scanner: <errors> errors, <warnings> warnings -> <errors>, <warnings>
| file:line | ID | decision | fix |
Behaviour changes: <callers affected, one per line>
Tests: <added, run, result>
Left for a person: <items>
```

### Starting prompt

```
Read javascript-error-handling.md. Scan <paths> for error-handling problems.
Runtime: <browser|node|both>. Errors go to: <logger or vendor>.
Run the scanner and report findings as file, line, ID and proposed fix.
Do not edit until I approve a batch. Do not add a reporting service.
```

Code templates (HTTP error class, serialiser, global handlers, React boundary, forced-failure test) are in `references/patterns.md`. Read it only when a step names a template.

## Failure modes and anti-patterns

| # | Failure | How to spot it | Do instead |
|---|---|---|---|
| 1 | Catch, log, carry on. The caller thinks it worked | E1, E2. A feature "works" with no data and no error in the UI | Step 3 table. Recover with a visible fallback or let it propagate |
| 2 | Context lost on rethrow or log | E3, E8. Logs show a message and no original stack, or `{}` | `{ cause }`, serialiser, one report at the final handler |
| 3 | A global handler used as the recovery strategy | E6, E7. Process keeps serving after `uncaughtException`, state is undefined | Log, clean up, exit, restart from a supervisor |
| 4 | Assuming a React boundary covers everything | P2, or a boundary exists yet a click handler error shows a blank white screen | `try/catch` in handlers and async code, boundary per region |
| 5 | HTTP errors treated as success, or cross-origin errors with no detail | E5. `Unexpected token < in JSON` from an error page. `Script error.` in reports | `response.ok` check and `HttpError`. `crossorigin` plus the CORS header |

## Quality bar

Run against the output before handing over. Each item is a check, not an opinion.

1. Scanner exit code is 0, or each remaining warning is justified in the report.
2. No `catch` block is empty, and none only logs, unless it ends in a return of a documented default.
3. Every `throw new` inside a `catch` has `cause`, or the report says why not.
4. Every `fetch` result passes through a `response.ok` check or a named wrapper that throws.
5. No `throw` of a string or object literal remains (`grep -nE "throw\s+[\"'\`{0-9]"` returns nothing).
6. No log call receives a raw `Error`, request body, header or token.
7. Each runtime has one global-handler registration or a stated reason for none.
8. Each fix has a forced-failure test, or the report lists which do not and why.
9. Behaviour changes for callers are listed with file and line.
10. The diff touches only flagged lines and the files needed for the new error class.

## Cost

Figures are estimates from this skill's own count on the final files (words × 1.35 for prose, characters ÷ 3.5 for code and tables). No tokeniser could be installed in the build environment, so none is measured. Rival figures are estimates from the visible text of pages I read.

| Item | Tokens | Status | Read condition |
|---|---|---|---|
| A. Description (always loaded) | 99 | Estimate | Every session with the skill installed |
| B. `SKILL.md` body | 5,043 | Estimate | When the skill fires |
| C. `references/appendix.md` | 714 | Estimate | Only for availability notes or the rival comparison |
| C. `references/patterns.md` | 1,238 | Estimate | Only when a step names a template or code block |
| Script output (typical run) | 127 | Estimate | Scanner output on the worked example (684 characters) |
| D. Typical run (A + B + script output) | 5,269 | Estimate | Common path, no references |
| D. Worst case (A + B + both references + 40 lines of output) | 7,673 | Estimate | Large input, every branch |
| E. Cheaper path | 5,269 | Estimate | Run the scanner, fix only flagged lines, skip both references |

Only one rival body was readable in full enough to size: about 740 to 880 tokens. This skill costs more to load. In return the scanner finds the patterns so the agent does not read every file to look for them.

## Handoffs

`promises-async-await.md` covers floating promises and combinators. `abortcontroller-cancellation.md` covers `AbortError` and `TimeoutError`: when a `catch` meets either, it is a cancellation, not a failure, so hand the catch to that skill. Both siblings read the same MDN pages, so none repeats another.

## Appendix: tool-specific notes

Kept short. Version details, support notes and the rival comparison are in `references/appendix.md`. Read it only for availability notes or the rival comparison.

| Tool | Note |
|---|---|
| Browser | `unhandledrejection` and `error` on `window`. Workers have their own scope: register inside the worker |
| Node | `process.on` hooks only. `--unhandled-rejections=strict` raises rejections as uncaught exceptions at once. **Verify other modes in the CLI docs for your version** |
| React | Class-component boundaries. `react-error-boundary` supplies a ready component |
| Vite and webpack | Failed chunk loads are an import problem, not an error-handling one. See `dynamic-import-code-splitting.md` |

## Sources

Last verified 7 November 2026 for every source.

- MDN, Error cause: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
- MDN, Window unhandledrejection event: https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
- Node.js docs, process: https://nodejs.org/api/process.html
- React reference, Component (error boundaries): https://react.dev/reference/react/Component
- javascript.info, Fetch: https://javascript.info/fetch
- javascript.info, Custom errors: https://javascript.info/custom-errors
- Sentry, What is Script Error: https://sentry.io/answers/script-error
- site24x7, Node.js error handling guide: https://www.site24x7.com/learn/nodejs-error-handling-guide.html
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Rival, wshobson error-handling-patterns: https://skills.browseract.com/skills/wshobson-agents-error-handling-patterns
- Rival, shipshitdev error-handling-expert (page metadata only): https://agent-skills.md/skills/shipshitdev/library/error-handling-expert
- Rival, pluginagentmarketplace nodejs error-handling (preview only): https://skills.sh/pluginagentmarketplace/custom-plugin-nodejs/error-handling

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/javascript/javascript-error-handling/
Download: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/javascript/javascript-error-handling/SKILL.md

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.
