Skip to content
Development
Skill

/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 plugin
beagle
81139 skills2 commands
Install
$ npx -y skills add existential-birds/beagle --skill remix-v2-error-boundaries-review --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/remix-v2-error-boundaries-review

Context preview

The summary Claude sees to decide when to auto-load this skill.

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.

SKILL.md

remix-v2-error-boundaries-review.SKILL.md
name: remix-v2-error-boundaries-review
description: 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.

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 `Response`s (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:

1. `isRouteErrorResponse(error)` → it was a thrown `Response`; read `error.status`, `error.statusText`, `error.data`. 2. `error instanceof Error` → real runtime error; read `error.message`. 3. 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 `Response`s do *not*).

Quick Reference

| Issue Type | Reference | |------------|-----------| | Missing route `ErrorBoundary`, props-on-boundary, narrowing-only `instanceof Error`, narrowing-only `isRouteErrorResponse` | [references/boundary-shape.md](references/boundary-shape.md) | | Return-instead-of-throw 4xx/5xx, swallowing `error.data`, throwing strings, missing `handleError` | [references/throw-response.md](references/throw-response.md) | | Missing root boundary, root boundary without `<Meta />`/`<Links />`/`<Scripts />`, `useLoaderData()` in root boundary | [references/root-boundary.md](references/root-boundary.md) | | `CatchBoundary` export, `useCatch` import, `v2_errorBoundary` future flag | [references/v1-holdovers.md](references/v1-holdovers.md) |

Review Checklist

  • [ ] `ErrorBoundary` declared `export function ErrorBoundary()` with **no** props
  • [ ] Error read via `useRouteError()`, not `useCatch()` and not a prop
  • [ ] Narrowing checks `isRouteErrorResponse(error)` **first**, then `error instanceof Error`, then fallback
  • [ ] `error.data` rendered defensively (typed/narrowed before going into JSX)
  • [ ] 4xx / 5xx in loaders/actions use `throw` (not `return`) for `Response` / `json`
  • [ ] Routes that can throw export their own `ErrorBoundary` (don't tear down parents for a widget failure)
  • [ ] Root `app/root.tsx` exports an `ErrorBoundary` that renders `<Meta />`, `<Links />`, and `<Scripts />`
  • [ ] Root boundary uses `useRouteLoaderData("root")` (not `useLoaderData()`) when reading root data
  • [ ] No `CatchBoundary` export anywhere; no `useCatch` import; no `future.v2_errorBoundary` in `remix.config.js`
  • [ ] `entry.server.tsx` exports `handleError` and pipes runtime errors to an error reporter
  • [ ] `handleError` does **not** assume thrown `Response`s flow through it (they don't)
  • [ ] Thrown values are `Response`/`json`/`Error` instances — 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 `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.
  • **Narrowing only with `isRouteErrorResponse(error)`** — Acceptable when the route demonstrably only throws `Response`s 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).
  • **Root `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 `Response`s** — By framework contract `handleError` only fires for runtime errors. The absence of `Response` handling is correct, not a gap.
  • **Nested `ErrorBoundary` returning 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` | WAR

Read more
Ships withbeagle

Image: NASA, Public Domain. Source Beagle is an Agent Skills marketplace: framework-aware code review, documentation, testing, architectural analysis, and git workflows for any compatible coding agent.

Get the whole plugin

Other skills on beagle.