Remix v2 error boundaries review
Skill existential-birds/beagle/plugins/beagle-react/skills/remix-v2-error-boundaries-review
Reviews Remix v2 error-handling code for the unified ErrorBoundary, isRouteErrorResponse narrowing, throw-vs-return, root boundary scaffolding, and v1 holdovers (CatchBoundary, useCatch). Use when reviewing routes that throw or define ErrorBoundary in a Remix v2 codebase.From its SKILL.md
npx -y skills add existential-birds/beagle --skill remix-v2-error-boundaries-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
9.5 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it
Remix v2 Error Boundaries Code Review
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/.
v2 Boundary Model (read first)
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 thrownResponse; readerror.status,error.statusText,error.data.error instanceof Error→ real runtime error; readerror.message.- else → unknown thrown value; render a generic fallback.
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).
Quick Reference
| 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 |
Review Checklist
-
ErrorBoundarydeclaredexport function ErrorBoundary()with no props - Error read via
useRouteError(), notuseCatch()and not a prop - Narrowing checks
isRouteErrorResponse(error)first, thenerror instanceof Error, then fallback -
error.datarendered defensively (typed/narrowed before going into JSX) - 4xx / 5xx in loaders/actions use
throw(notreturn) forResponse/json - Routes that can throw export their own
ErrorBoundary(don't tear down parents for a widget failure) - Root
app/root.tsxexports anErrorBoundarythat renders<Meta />,<Links />, and<Scripts /> - Root boundary uses
useRouteLoaderData("root")(notuseLoaderData()) when reading root data - No
CatchBoundaryexport anywhere; nouseCatchimport; nofuture.v2_errorBoundaryinremix.config.js -
entry.server.tsxexportshandleErrorand pipes runtime errors to an error reporter -
handleErrordoes not assume thrownResponses flow through it (they don't) - Thrown values are
Response/json/Errorinstances — never plain strings or POJOs
Valid Patterns (Do NOT Flag)
These are correct Remix v2 usage and must not be reported as issues:
- Route without
ErrorBoundarythat intentionally inherits from a parent — Boundaries cascade up. A child route may omitErrorBoundaryso the parent (or root) renders the fallback. Only flag if the route handles user-distinct error UX and a parent boundary cannot. throw new Response(...)orthrow json(...)from a loader/action — The canonical way to signal 404/401/403/etc. This is not "using exceptions for control flow"; it is documented v2 contract.- Narrowing only with
isRouteErrorResponse(error)— Acceptable when the route demonstrably only throwsResponses and has no render-time crash risk. Severity is ADVISORY at most; suggest adding aninstanceof Errorbranch for defense-in-depth, do not flag as a bug. ErrorBoundarythat does not calluseRouteError()— Valid when the boundary renders a static "Something went wrong" fallback intentionally (e.g. marketing pages that don't want to surface error detail).- Root
ErrorBoundarycallinguseRouteLoaderData("root")and gettingundefined— Documented defensive pattern (root loader may have thrown). Do not flag theundefinedhandling as "dead code." handleErrorreturning early onrequest.signal.aborted— Documented noise filter, not a swallowed error.handleErrornot handling thrownResponses — By framework contracthandleErroronly fires for runtime errors. The absence ofResponsehandling is correct, not a gap.- Nested
ErrorBoundaryreturning a bare fragment (no<html>/<body>) — Only the root boundary owns the document. Nested boundaries render inside parent layouts and must not include document tags.
Severity guidance
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) |
Hard gates (before writing findings)
Run in order. Do not draft user-facing findings until every gate passes for the batch you are about to report.
-
Location evidence — Pass: Each issue lists the repo path to the route module (or
app/root.tsx, orapp/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 toapp/root.tsxis not reportable. -
Exemption check — Pass: For each issue, you can state in one line why it is not covered by Valid Patterns (Do NOT Flag). In particular: confirm a missing
ErrorBoundaryis not a deliberate cascade to a parent boundary; confirm anisRouteErrorResponse-only narrowing is not on a route that demonstrably only throws Responses (downgrade to ADVISORY in that case). -
v1-vs-v2 marker check — Pass: Before writing the finding, 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. -
Protocol — Pass: You completed the Pre-Report Verification Checklist in review-verification-protocol for this review.
Review Questions
- Does every route that can throw (loader, action, or render) have an
ErrorBoundaryat the right level — local where the recovery UI matters, parent/root where cascade is intentional? - Does each
ErrorBoundarycalluseRouteError()(notuseCatch(), not props) and narrowisRouteErrorResponsefirst? - Are 4xx / 5xx control flows using
throw(notreturn) so the boundary actually fires? - Does
app/root.tsxexport anErrorBoundarywith<Meta />,<Links />, and<Scripts />, and useuseRouteLoaderData("root")defensively? - Are there any v1 markers left (
CatchBoundary,useCatch,v2_errorBoundary,({ error })prop signature)? - Is
handleErrorpresent inentry.server.tsxfor runtime-error observability, with the correct contract (no Response handling)?
Additional Documentation
- Reviewing the
ErrorBoundaryexport shape, hook usage, or narrowing → references/boundary-shape.md - Reviewing thrown
Response/jsonpatterns,handleError, or return-vs-throw → references/throw-response.md - Reviewing
app/root.tsxboundary scaffolding → references/root-boundary.md - Detecting v1 holdovers (
CatchBoundary,useCatch,v2_errorBoundary) → references/v1-holdovers.md - Remix v2 ErrorBoundary docs: https://remix.run/docs/en/main/route/error-boundary
- Remix v2 error handling guide: https://remix.run/docs/en/main/guides/errors
- Remix v2
entry.server/handleErrordocs: https://remix.run/docs/en/main/file-conventions/entry.server
What ships with it: 4 files
25.5 KB alongside SKILL.md
references/
- boundary-shape.md6.1 KB
- root-boundary.md6.4 KB
- throw-response.md7.1 KB
- v1-holdovers.md6.0 KB
Gives 0 of the 12 instructions most review quality skills give in ~2.3k tokens
Counted across 1,048 of the 1,783 authors here whose files we hold, read 2026-08-07
- Ask questions one at a timein 81 of 1048, across 64 files
- Provide a recommended answer for each questionin 73 of 1048, across 50 files
- Explore the codebase instead of asking answerable questionsin 66 of 1048, across 42 files
- Resolve dependencies between decisions one-by-onein 42 of 1048, across 17 files
- Interview the user relentlessly about the planin 38 of 1048, across 13 files
- Order findings by severityin 31 of 1048
- Resolve each branch of the decision treein 27 of 1048, across 5 files
- Run a grilling sessionin 26 of 1048, across 5 files
- Update CONTEXT.md immediately when a term is resolvedin 26 of 1048, across 11 files
- Propose precise canonical terms for vague languagein 25 of 1048, across 7 files
- Create documentation files lazilyin 24 of 1048, across 5 files
- Assign severity to every findingin 24 of 1048
Said here and by no other author read
- Verify ErrorBoundary takes no props.
- Verify errors are read via useRouteError.
- Narrow with isRouteErrorResponse first, then instanceof Error.
- Defensively render error.data.
- Throw Responses for 4xx and 5xx errors.
- Export handleError in entry.server.tsx.
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.