关于 remix-v2-error-boundaries-review
面向 TypeScript Remix v2 项目的错误边界代码审查技能,适用于检查路由错误处理和 v1 迁移遗留问题。它涵盖边界结构、异常响应、根边界及错误报告规则。
主要功能
- 01检查 ErrorBoundary 导出与错误收窄
- 02审查 4xx 和 5xx 的抛出方式
- 03检查根边界文档结构
- 04识别 CatchBoundary 等 v1 遗留
- 05提供误报豁免与严重级别指引
适用场景
- 01审查 Remix 路由的错误边界
- 02检查 4xx 响应是否触发边界
- 03排查 Remix v1 错误处理遗留
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill remix-v2-error-boundaries-review安装后在 Claude Code 中输入/remix-v2-error-boundaries-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/boundary-shape.mdroot-boundary.mdthrow-response.mdv1-holdovers.md详细介绍
Remix v2 错误边界代码审查
适用于从 @remix-run/* 导入内容的 TypeScript 路由模块。此主题没有相关的知识技能;下文内联概述了核心思维模型,详情见 references/。
v2 边界模型(请先阅读)
Remix v2 将 v1 的 CatchBoundary 和 ErrorBoundary 合并为单个 ErrorBoundary 路由模块导出。框架会在抛出 Response(例如 throw new Response(...)、throw json(...))和抛出运行时错误(loader/action/render 异常)时调用它。在边界中,使用 useRouteError() 钩子读取错误值,然后按以下顺序缩小类型范围:
isRouteErrorResponse(error)→ 这是抛出的Response;读取error.status、error.statusText、error.data。error instanceof Error→ 这是实际的运行时错误;读取error.message。- 否则 → 这是未知的抛出值;渲染通用回退内容。
边界不接受任何 props。CatchBoundary、useCatch 和 future.v2_errorBoundary 标志均已移除——发现其中任何一个,都说明代码仍沿用 v1 的旧写法。错误会由最近的 ErrorBoundary 渲染;如果没有,则向上冒泡至根边界。根边界会重新挂载整个文档,因此必须渲染 <Meta />、<Links /> 和 <Scripts />。只有抛出的 loader/action 结果才会到达边界——return json(...) 即使状态码为 4xx,也属于成功的 loader,而不是错误。服务器端运行时错误还会流经可选的 entry.server.tsx handleError 导出(抛出的 Response 不会)。
快速参考
| 问题类型 | 参考 |
|---|---|
缺少路由 ErrorBoundary、边界带 props、仅用 instanceof Error 缩小类型范围、仅用 isRouteErrorResponse 缩小类型范围 | references/boundary-shape.md |
4xx/5xx 该抛出却用了 return、忽略 error.data、抛出字符串、缺少 handleError | references/throw-response.md |
缺少根边界、根边界未包含 <Meta />/<Links />/<Scripts />、在根边界中使用 useLoaderData() | references/root-boundary.md |
导出 CatchBoundary、导入 useCatch、使用 v2_errorBoundary future 标志 | references/v1-holdovers.md |
审查清单
-
ErrorBoundary声明为export function ErrorBoundary(),且不带 props - 通过
useRouteError()读取错误,而不是使用useCatch()或 prop - 类型缩小检查先执行
isRouteErrorResponse(error),然后是error instanceof Error,最后提供回退处理 - 安全地渲染
error.data(放入 JSX 前先进行类型标注/缩小) - loader/action 中的 4xx/5xx 使用
throw(而不是return)抛出Response/json - 可能抛出错误的路由应导出自己的
ErrorBoundary(不要因一个组件失败而拆除父级) - 根目录
app/root.tsx导出一个渲染<Meta />、<Links />和<Scripts />的ErrorBoundary - 根边界读取根数据时使用
useRouteLoaderData("root")(而不是useLoaderData()) - 任何位置都没有导出
CatchBoundary;没有导入useCatch;remix.config.js中没有future.v2_errorBoundary -
entry.server.tsx导出handleError,并将运行时错误传递给错误报告工具 -
handleError不应假设抛出的Response会经过它(实际上不会) - 抛出的值应为
Response/json/Error实例,绝不能是普通字符串或 POJO
有效模式(不要标记)
以下是正确的 Remix v2 用法,不应报告为问题:
- 有意从父级继承、但未设置
ErrorBoundary的路由 — 边界会逐级向上查找。子路由可以省略ErrorBoundary,由父级(或根路由)渲染回退界面。只有当该路由处理了与用户相关的特定错误体验,且父级边界无法处理时,才应标记。 - 在 loader/action 中使用
throw new Response(...)或throw json(...)— 这是表示 404/401/403 等状态的标准方式。这不是“将异常用于控制流”;这是文档明确规定的 v2 契约。 - 仅使用
isRouteErrorResponse(error)进行类型收窄 — 如果路由明确只会抛出Response,且不存在渲染期间崩溃的风险,这样做可以接受。严重性最高为 ADVISORY;建议添加instanceof Error分支以加强防御,但不要将其标记为 Bug。 - 不调用
useRouteError()的ErrorBoundary— 如果边界有意渲染静态的“出了点问题”回退界面(例如不希望显示错误详情的营销页面),这是有效用法。 - 根
ErrorBoundary调用useRouteLoaderData("root")并得到undefined— 这是文档所述的防御性模式(根 loader 可能已抛出错误)。不要将对undefined的处理标记为“死代码”。 handleError在request.signal.aborted时提前返回 — 这是文档所述的噪声过滤机制,并非吞掉错误。handleError未处理抛出的Response— 根据框架契约,handleError只会针对运行时错误触发。未处理Response是正确的,并非缺漏。- 嵌套的
ErrorBoundary直接返回片段(不含<html>/<body>) — 只有根边界负责整个文档。嵌套边界在父级布局内部渲染,不得包含文档标签。
严重性指南
除非代码库已明确规定其他等级,否则请使用以下默认值:
| 模式 | 默认严重性 |
|---|---|
v2 代码库中导出 CatchBoundary 或导入 useCatch | BLOCKER(会导致构建失败或产生死代码) |
根 ErrorBoundary 缺少 <Scripts /> | BLOCKER(错误页面无路可走) |
ErrorBoundary 使用 ({ error }) 这一 v1 属性签名 | WARN(运行时会静默得到 undefined) |
4xx 状态使用 return json(...) 而非 throw | WARN(边界永远不会触发) |
有渲染崩溃风险的路由缺少 instanceof Error 分支 | WARN |
仅抛出 Response 的路由缺少 instanceof Error 分支 | ADVISORY |
根边界中使用 useLoaderData()(而非 useRouteLoaderData) | WARN(可能导致循环) |
entry.server.tsx 中缺少 handleError | ADVISORY(可观测性缺口,并非 Bug) |
硬性门槛(撰写问题报告前)
按顺序执行。在本批次的所有门槛都通过之前,不要起草面向用户的问题报告。
-
位置证据 — 通过: 每个问题都列出路由模块的仓库路径(或
app/root.tsx、app/entry.server.tsx),以及你实际读取的文件中的行号范围或简短原文引述(不能凭记忆,也不能只根据差异猜测)。“根边界有误”如果没有提供app/root.tsx的路径,则不能报告。 -
豁免检查 — 通过: 对每个问题,你都能用一行说明它为何不属于 有效模式(不要标记)的范围。尤其要确认:缺少
ErrorBoundary并非有意将错误逐级传递给父级边界;仅使用isRouteErrorResponse缩窄类型的路由,并非已明确只会抛出 Response 的路由(若确实如此,应降级为 ADVISORY)。 -
v1 与 v2 标记检查 — 通过: 撰写问题报告前,使用 grep 检查路由模块(对于横切问题,则检查整个仓库)是否包含:
CatchBoundary、useCatch、v2_errorBoundary、ErrorBoundary({ error、ErrorBoundary({error。如果出现其中任意一项,该问题就是 v1 遗留项(加载 references/v1-holdovers.md),必须如此标记——不能将其笼统地标记为“缺少错误处理”问题。如果均未出现,则代码符合 v2 结构,问题应针对 v2 的正确性。 -
流程 — 通过: 已针对本次审查完成 审查前验证清单。
审查问题
- 每个可能抛出错误的路由(loader、action 或渲染)是否都在正确层级设置了
ErrorBoundary——恢复 UI 重要时使用局部边界,有意逐级传递时使用父级或根级边界? - 每个
ErrorBoundary是否都调用useRouteError()(而非useCatch()或 props),并首先使用isRouteErrorResponse缩窄类型? - 4xx / 5xx 控制流是否使用
throw(而非return),以确保触发边界? app/root.tsx是否导出了带有<Meta />、<Links />和<Scripts />的ErrorBoundary,并以防御性方式使用useRouteLoaderData("root")?- 是否还存在 v1 标记(
CatchBoundary、useCatch、v2_errorBoundary、({ error })属性签名)? entry.server.tsx中是否存在handleError,以便观察运行时错误,且符合正确的契约(不处理 Response)?
补充文档
- 审查
ErrorBoundary导出结构、钩子用法或类型缩窄 → references/boundary-shape.md - 审查抛出
Response/json模式、handleError或返回与抛出的区别 → references/throw-response.md - 审查
app/root.tsx边界框架 → references/root-boundary.md - 检测 v1 遗留项(
CatchBoundary、useCatch、v2_errorBoundary)→ references/v1-holdovers.md - Remix v2 ErrorBoundary 文档:https://remix.run/docs/en/main/route/error-boundary
- Remix v2 错误处理指南:https://remix.run/docs/en/main/guides/errors
- Remix v2
entry.server/handleError文档:https://remix.run/docs/en/main/file-conventions/entry.server