remix-v2-error-boundaries-review — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited remix-v2-error-boundaries-review (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Targets TypeScript route modules importing from @remix-run/*. No sibling knowledge skill exists for this topic; the canonical mental model is summarized inline below and expanded in references/.
Remix v2 unified v1's CatchBoundary + ErrorBoundary into a single ErrorBoundary route-module export. The framework calls it for both thrown Responses (e.g. throw new Response(...), throw json(...)) and thrown runtime errors (loader/action/render exceptions). Inside the boundary you read the value with the useRouteError() hook, then narrow in this order:
isRouteErrorResponse(error) → it was a thrown Response; readerror.status, error.statusText, error.data.
error instanceof Error → real runtime error; read error.message.The boundary takes no props. CatchBoundary, useCatch, and the future.v2_errorBoundary flag are all gone — finding any of them is a v1 holdover. Errors render the nearest ErrorBoundary and bubble to the root if none exists; the root boundary remounts the whole document, so it must render <Meta />, <Links />, and <Scripts />. Only thrown loader/action results reach the boundary — a return json(...) with a 4xx status is a successful loader, not an error. Server-side runtime errors also flow through an optional entry.server.tsx handleError export (thrown Responses do not).
| Issue Type | Reference |
|---|---|
Missing route ErrorBoundary, props-on-boundary, narrowing-only instanceof Error, narrowing-only isRouteErrorResponse | references/boundary-shape.md |
Return-instead-of-throw 4xx/5xx, swallowing error.data, throwing strings, missing handleError | references/throw-response.md |
Missing root boundary, root boundary without <Meta />/<Links />/<Scripts />, useLoaderData() in root boundary | references/root-boundary.md |
CatchBoundary export, useCatch import, v2_errorBoundary future flag | references/v1-holdovers.md |
ErrorBoundary declared export function ErrorBoundary() with no propsuseRouteError(), not useCatch() and not a propisRouteErrorResponse(error) first, then error instanceof Error, then fallbackerror.data rendered defensively (typed/narrowed before going into JSX)throw (not return) for Response / jsonErrorBoundary (don't tear down parents for a widget failure)app/root.tsx exports an ErrorBoundary that renders <Meta />, <Links />, and <Scripts />useRouteLoaderData("root") (not useLoaderData()) when reading root dataCatchBoundary export anywhere; no useCatch import; no future.v2_errorBoundary in remix.config.jsentry.server.tsx exports handleError and pipes runtime errors to an error reporterhandleError does not assume thrown Responses flow through it (they don't)Response/json/Error instances — never plain strings or POJOsThese are correct Remix v2 usage and must not be reported as issues:
ErrorBoundary so the parent (or root) renders the fallback. Only flag if the route handles user-distinct error UX and a parent boundary cannot.Responses and has no render-time crash risk. Severity is ADVISORY at most; suggest adding an instanceof Error branch for defense-in-depth, do not flag as a bug.undefined handling as "dead code."handleError only fires for runtime errors. The absence of Response handling is correct, not a gap.Use these defaults unless the codebase has documented a different scale:
| Pattern | Default severity |
|---|---|
CatchBoundary export or useCatch import in v2 codebase | BLOCKER (build-breaking or dead code) |
Root ErrorBoundary missing <Scripts /> | BLOCKER (dead-end error page) |
ErrorBoundary with ({ error }) v1 prop signature | WARN (silent runtime undefined) |
return json(...) for 4xx instead of throw | WARN (boundary never fires) |
Missing instanceof Error branch on a route with render-crash risk | WARN |
Missing instanceof Error branch on a Response-only route | ADVISORY |
useLoaderData() (vs useRouteLoaderData) in root boundary | WARN (latent loop) |
Missing handleError in entry.server.tsx | ADVISORY (observability gap, not a bug) |
Run in order. Do not draft user-facing findings until every gate passes for the batch you are about to report.
the route module (or app/root.tsx, or app/entry.server.tsx) and either a line range or a short verbatim quote from the file you read (not from memory or diff-only guesswork). "The root boundary is wrong" without a path to app/root.tsx is not reportable.
line why it is not covered by Valid Patterns (Do NOT Flag). In particular: confirm a missing ErrorBoundary is not a deliberate cascade to a parent boundary; confirm an isRouteErrorResponse-only narrowing is not on a route that demonstrably only throws Responses (downgrade to ADVISORY in that case).
grep the route module (and the repo at large for cross-cutting issues) for: CatchBoundary, useCatch, v2_errorBoundary, ErrorBoundary({ error, ErrorBoundary({error. If any of these appear, the finding is a v1 holdover (load references/v1-holdovers.md) and must be labeled as such — not as a generic "missing error handling" issue. If none appear, the code is v2-shape and the finding is about v2 correctness.
Checklist in review-verification-protocol for this review.
ErrorBoundary at the right level — local where the recovery UI matters, parent/root where cascade is intentional?
ErrorBoundary call useRouteError() (not useCatch(),not props) and narrow isRouteErrorResponse first?
throw (not return) so theboundary actually fires?
app/root.tsx export an ErrorBoundary with <Meta />,<Links />, and <Scripts />, and use useRouteLoaderData("root") defensively?
CatchBoundary, useCatch,v2_errorBoundary, ({ error }) prop signature)?
handleError present in entry.server.tsx for runtime-errorobservability, with the correct contract (no Response handling)?
ErrorBoundary export shape, hook usage, or narrowing → references/boundary-shape.mdResponse / json patterns, handleError, or return-vs-throw → references/throw-response.mdapp/root.tsx boundary scaffolding → references/root-boundary.mdCatchBoundary, useCatch, v2_errorBoundary) → references/v1-holdovers.mdentry.server / handleError docs: https://remix.run/docs/en/main/file-conventions/entry.server~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.