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; read error.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 , , and . 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 //, 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 , , and 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 that intentionally inherits from a parent — Boundaries cascade up. A child route may omit ErrorBoundary so 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(...) or throw 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.isRouteErrorResponse(error) — Acceptable when the route demonstrably only throws 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.ErrorBoundary that does not call useRouteError() — Valid when the boundary renders a static "Something went wrong" fallback intentionally (e.g. marketing pages that don't want to surface error detail).ErrorBoundary calling useRouteLoaderData("root") and getting undefined — Documented defensive pattern (root loader may have thrown). Do not flag the undefined handling as "dead code."handleError returning early on request.signal.aborted — Documented noise filter, not a swallowed error.handleError not handling thrown Responses — By framework contract handleError only fires for runtime errors. The absence of Response handling is correct, not a gap.ErrorBoundary returning a bare fragment (no / ) — Only the root boundary owns the document. Nested boundaries render inside parent layouts and must not include document tags.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 | 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 , , and , 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共 1 个版本