JavaScript skill 4 of 12

JavaScript Error Handling SKILL.md: Fix Swallowed Errors and Lost Causes

The JavaScript Error Handling skill makes Claude, Cursor or another AI agent find and fix empty catch blocks, rethrows that lose the cause, fetches that treat 404 and 500 as success and Node processes that survive uncaughtException. A scanner lists each pattern by file and line, and the agent edits only flagged lines in batches you approve.

javascript-error-handling/SKILL.md, version 1.0.0, 3,551 words, plus scripts/errors_check.py and two reference files.

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.

RequestUse instead
Floating promises, forEach(async), choosing all, allSettled, any or racepromises-async-await.md
AbortError, TimeoutError, cancelling requestsabortcontroller-cancellation.md
Retry and backoff policy, caching, request stateA state and data fetching skill. Out of scope
Choosing or configuring an error-reporting vendorOut 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.

InputIf 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

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as javascript-error-handling/SKILL.md.

---
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.

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.

FailureClassHandle where
Network down, 4xx/5xx, missing file, invalid user input, timeoutOperational. ExpectedThe 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 requiredProgrammer error. A bugDo not catch to recover. Let it reach the boundary and the global handler, report it, fix the code
Third-party library throwsUnknownCatch 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

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.

IDLevelMeaningFix in step
E1errorEmpty catch or .catch(() => {})3
E2warncatch only calls console.*3
E3warnthrow new X(...) in a catch with no cause3, 5
E4errorthrow "text", throw {…} or throw 425
E5warnfetch in a file with no .ok or .status check4
E6erroruncaughtException handler that does not exit7
E7warnunhandledRejection handler that neither exits nor reports7
E8warnJSON.stringify(err)6
E9warnCaught variable never used3
E10warncatch (e) { throw e }3
E11warnpreventDefault() inside an unhandledrejection handler7
P1infoNo global error or rejection handler in the scanned files7
P2infoReact imported but no error boundary in the scanned files8

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:

SituationEdit
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 recoverDelete the catch, or wrap with { cause } and rethrow (step 5)
Cleanup is needed either wayUse 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.

RuntimeRule
BrowserRegister 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 unhandledRejectionLog 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 uncaughtExceptionLog, 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 uncaughtExceptionMonitorObserve-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

#FailureHow to spot itDo instead
1Catch, log, carry on. The caller thinks it workedE1, E2. A feature "works" with no data and no error in the UIStep 3 table. Recover with a visible fallback or let it propagate
2Context lost on rethrow or logE3, E8. Logs show a message and no original stack, or {}{ cause }, serialiser, one report at the final handler
3A global handler used as the recovery strategyE6, E7. Process keeps serving after uncaughtException, state is undefinedLog, clean up, exit, restart from a supervisor
4Assuming a React boundary covers everythingP2, or a boundary exists yet a click handler error shows a blank white screentry/catch in handlers and async code, boundary per region
5HTTP errors treated as success, or cross-origin errors with no detailE5. Unexpected token < in JSON from an error page. Script error. in reportsresponse.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.

ItemTokensStatusRead condition
A. Description (always loaded)99EstimateEvery session with the skill installed
B. SKILL.md body5,043EstimateWhen the skill fires
C. references/appendix.md714EstimateOnly for availability notes or the rival comparison
C. references/patterns.md1,238EstimateOnly when a step names a template or code block
Script output (typical run)127EstimateScanner output on the worked example (684 characters)
D. Typical run (A + B + script output)5,269EstimateCommon path, no references
D. Worst case (A + B + both references + 40 lines of output)7,673EstimateLarge input, every branch
E. Cheaper path5,269EstimateRun 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.

ToolNote
Browserunhandledrejection and error on window. Workers have their own scope: register inside the worker
Nodeprocess.on hooks only. --unhandled-rejections=strict raises rejections as uncaught exceptions at once. Verify other modes in the CLI docs for your version
ReactClass-component boundaries. react-error-boundary supplies a ready component
Vite and webpackFailed 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.

What this skill does

Error handling goes wrong in quiet ways. An empty catch hides a failure from everyone. A rethrow drops the original stack unless it passes cause. fetch resolves on a 404 or 500, so the next line parses an HTML error page as JSON. JSON.stringify(error) returns {} because the message, stack and cause are not enumerable, which I checked on Node 22 on 7 November 2026.

The skill first decides which layer can act on each failure, because a layer that cannot retry, substitute or tell the user should not catch the error. It then runs scripts/errors_check.py, and the agent fixes catch blocks, adds response.ok checks and a typed HttpError, wraps with cause, logs through a redacting serialiser and limits global handlers to reporting and shutdown. It adds no reporting service.

Who it is for

Use it if you are a frontend or Node developer, a reviewer of AI-generated code full of tidy try and catch blocks that do nothing, or the owner of an app where failures show as blank screens, empty lists or logs that read {}. It fits when a Node process keeps serving after uncaughtException, when a React error boundary misses a click handler error, or when reports show Script error. with no line number. It does not cover promise combinators, cancellation, retry policy or choosing an error-reporting vendor.

What is inside

The file is 3,551 words (version 1.0.0) with one script and two reference files. It holds:

  • Five guardrails, including never keeping a Node process alive after uncaughtException and never putting raw errors, request bodies or headers into logs.
  • Six inputs the agent must have first, each with the exact question to ask.
  • A nine-step workflow, from who can act through scanning and fixes to verification.
  • One tool, scripts/errors_check.py, a standard-library scanner with thirteen codes that prints file, line and a fixed message, never source text.
  • A worked example with real output: 3 errors and 5 warnings before, none after.
  • A report template, a starting prompt, five failure modes and a ten-item quality bar.
  • Two reference files, read only when a step points to them: code templates, and availability notes with the rival comparison.
ItemTokensStatusRead when
Description99EstimateEvery session with the skill installed
SKILL.md body5,043EstimateWhen the skill fires
references/patterns.md1,238EstimateOnly when a step names a template or code block
references/appendix.md714EstimateOnly for availability notes or the rival comparison
Typical run5,269EstimateDescription, body and scanner output
Worst case7,673EstimateBoth references and 40 lines of output
Cheaper path5,269EstimateRun the scanner, fix flagged lines, skip the references

The figures come from the skill’s own count (words × 1.35 for prose, characters ÷ 3.5 for code and tables), not from a model tokeniser. The only rival whose body I could size has about 740 to 880 tokens by the same method, and none of the three has a scanner.

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as javascript-error-handling/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. The scanner needs Python 3 and nothing else. Then send the starting prompt from the Templates section.

Before you install. Read the script first. It reads files and writes nothing. The skill tells the agent to edit only on a branch in batches you approve, never to run your code against live services, never to paste error messages, stacks or tokens into prompts, and never to add a reporting service you did not name. Install skills only from sources you trust.

A worked example

An invented 38-line file, orders.js, fetches an order without checking response.ok, wraps a database error in a new Error with no cause, has an empty catch around JSON.parse, throws the string “no items”, logs an error inside a catch and carries on, calls JSON.stringify on an error and registers an uncaughtException handler that never exits. The scanner prints 3 errors and 5 warnings.

The agent decides which layer can recover. loadOrder cannot, so it throws HttpError. saveOrder cannot, so it wraps with cause. parseCart can, so it reports once and returns an empty cart. validate throws a TypeError. The process handler logs, sets process.exitCode and exits. The re-run prints 0 errors and 0 warnings, and the report lists the callers that now see HttpError.

Common mistakes

The skill is built around five ways this goes wrong:

  • Catch, log and carry on. The caller believes the work succeeded. The skill recovers with a visible fallback or lets the error propagate.
  • Losing context on rethrow. The original stack vanishes, or the log shows {}. The skill passes cause and logs through a serialiser.
  • Using a global handler to recover. After uncaughtException the process state is undefined. The skill logs, cleans up and exits.
  • Trusting a React boundary to cover everything. Event handlers and async code sit outside it. The skill adds try and catch there.
  • Treating HTTP errors as success. A 500 resolves, then the JSON parse fails. The skill checks response.ok and throws a typed error.

Promises and Async/Await covers floating promises, forEach(async) and the choice of combinator, which is where an unhandled rejection usually starts. AbortController Cancellation covers AbortError and TimeoutError, which a catch block should treat as cancellations. Dynamic Import and Code Splitting covers failed chunk loads after a deploy.

Frequently asked questions

Decide whether that layer can recover. If it can, keep the catch, recover and report the error once. If it cannot, delete the catch or wrap the error with cause and rethrow. An empty catch, or one that only calls console.log, tells the caller the work succeeded. The scanner flags both.

Pass a second argument: new Error(“Could not save order”, { cause: err }). The original stays reachable as error.cause. MDN lists it as Baseline widely available since September 2021. Cause can be any value, so check instanceof Error first. The scanner flags a throw new inside a catch with no cause.

fetch resolves once the server sends headers and rejects only when the request cannot be made. A 404 or 500 still resolves, and response.ok is true only for statuses 200 to 299, according to javascript.info. Check response.ok before reading the body and throw a typed error. The scanner flags fetch calls with no check.

The message, stack and cause of an Error are not enumerable, and JSON.stringify skips non-enumerable properties. I ran JSON.stringify(new Error(“x”, { cause: 1 })) on Node 22 on 7 November 2026 and got {}. Log name, message, stack and the cause chain explicitly.

No. The Node documentation says the process state is undefined after an uncaught exception and warns against using the handler to carry on. Log, do synchronous cleanup, set process.exitCode and exit, then let a supervisor restart the service.

Boundaries catch errors thrown while React renders their children. The React reference lists what they miss: event handlers, async code such as setTimeout, server-side rendering and errors thrown by the boundary itself. Use try and catch in handlers and set error state.

The description is about 99 tokens and loads in every session. The SKILL.md body is about 5,043 tokens when the skill fires. A typical run is about 5,269 tokens with scanner output, the cheaper path is the same, and the worst case with both references is about 7,673. All figures are estimates, not from a model tokeniser.

Changelog

  • 1.0.0, 7 November 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.