Remix错误边界 #
一、错误处理概述 #
Remix 提供了强大的错误处理机制,每个路由都可以有自己的错误边界,实现优雅的错误展示和恢复。
二、ErrorBoundary #
2.1 基本用法 #
tsx
import { useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div className="error-container">
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</div>
);
}
return (
<div className="error-container">
<h1>出错了</h1>
<p>{error.message}</p>
</div>
);
}
2.2 错误类型 #
tsx
import {
useRouteError,
isRouteErrorResponse,
useNavigate
} from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteErrorResponse();
const navigate = useNavigate();
// 路由响应错误(如404、500)
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
<button onClick={() => navigate(-1)}>返回</button>
</div>
);
}
// JavaScript错误
if (error instanceof Error) {
return (
<div>
<h1>应用错误</h1>
<p>{error.message}</p>
<pre>{error.stack}</pre>
</div>
);
}
// 未知错误
return (
<div>
<h1>未知错误</h1>
<p>请刷新页面重试</p>
</div>
);
}
三、抛出错误 #
3.1 在loader中抛出 #
tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
if (!post) {
throw new Response("文章不存在", { status: 404 });
}
return json({ post });
}
3.2 在action中抛出 #
tsx
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get("title");
if (!title || title.toString().trim().length < 3) {
throw new Response(
JSON.stringify({ error: "标题至少需要3个字符" }),
{
status: 400,
headers: { "Content-Type": "application/json" }
}
);
}
// ...
}
3.3 使用json抛出 #
tsx
import { json } from "@remix-run/node";
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.id);
if (!post) {
throw json(
{ message: "文章不存在", code: "NOT_FOUND" },
{ status: 404 }
);
}
return json({ post });
}
四、嵌套错误边界 #
4.1 层级错误处理 #
每个路由都可以有自己的错误边界:
tsx
// app/routes/admin.tsx
export function ErrorBoundary() {
const error = useRouteError();
return (
<div className="admin-error">
<h2>管理后台错误</h2>
<p>{error.message}</p>
<Link to="/admin">返回仪表板</Link>
</div>
);
}
// app/routes/admin.users.tsx
export function ErrorBoundary() {
const error = useRouteError();
return (
<div className="users-error">
<h3>用户管理错误</h3>
<p>{error.message}</p>
<Link to="/admin/users">重试</Link>
</div>
);
}
4.2 错误冒泡 #
如果没有定义错误边界,错误会向上冒泡:
text
admin.users.tsx (无ErrorBoundary)
↓
admin.tsx (有ErrorBoundary) ← 错误在这里捕获
↓
root.tsx (有ErrorBoundary)
五、根错误边界 #
5.1 在root.tsx中定义 #
tsx
import {
useRouteError,
isRouteErrorResponse,
Links,
Meta,
Scripts
} from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
return (
<html lang="zh-CN">
<head>
<Meta />
<Links />
<title>出错了</title>
</head>
<body>
<div className="error-page">
<h1>应用程序错误</h1>
{isRouteErrorResponse(error) ? (
<div>
<h2>{error.status} {error.statusText}</h2>
<p>{error.data}</p>
</div>
) : (
<div>
<h2>发生意外错误</h2>
<p>{error.message}</p>
</div>
)}
<a href="/">返回首页</a>
</div>
<Scripts />
</body>
</html>
);
}
六、自定义错误类 #
6.1 定义错误类 #
tsx
class NotFoundError extends Error {
constructor(message: string = "资源不存在") {
super(message);
this.name = "NotFoundError";
}
}
class UnauthorizedError extends Error {
constructor(message: string = "未授权") {
super(message);
this.name = "UnauthorizedError";
}
}
class ForbiddenError extends Error {
constructor(message: string = "禁止访问") {
super(message);
this.name = "ForbiddenError";
}
}
6.2 使用自定义错误 #
tsx
export async function loader({ params, request }: LoaderFunctionArgs) {
const user = await getUser(request);
if (!user) {
throw new UnauthorizedError("请先登录");
}
const post = await getPost(params.id);
if (!post) {
throw new NotFoundError("文章不存在");
}
if (post.authorId !== user.id) {
throw new ForbiddenError("无权访问此文章");
}
return json({ post });
}
6.3 处理自定义错误 #
tsx
export function ErrorBoundary() {
const error = useRouteError();
if (error instanceof NotFoundError) {
return (
<div className="not-found">
<h1>404</h1>
<p>{error.message}</p>
<Link to="/">返回首页</Link>
</div>
);
}
if (error instanceof UnauthorizedError) {
return (
<div className="unauthorized">
<h1>未授权</h1>
<p>{error.message}</p>
<Link to="/login">前往登录</Link>
</div>
);
}
if (error instanceof ForbiddenError) {
return (
<div className="forbidden">
<h1>禁止访问</h1>
<p>{error.message}</p>
</div>
);
}
return (
<div className="error">
<h1>出错了</h1>
<p>{error.message}</p>
</div>
);
}
七、错误恢复 #
7.1 重试机制 #
tsx
import { useRouteError, useRevalidator } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
const revalidator = useRevalidator();
return (
<div>
<h1>出错了</h1>
<p>{error.message}</p>
<button onClick={() => revalidator.revalidate()}>
重试
</button>
</div>
);
}
7.2 导航恢复 #
tsx
import { useRouteError, useNavigate, Link } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
const navigate = useNavigate();
return (
<div>
<h1>出错了</h1>
<p>{error.message}</p>
<div className="actions">
<button onClick={() => navigate(-1)}>返回上一页</button>
<Link to="/">返回首页</Link>
<button onClick={() => window.location.reload()}>刷新页面</button>
</div>
</div>
);
}
八、最佳实践 #
8.1 友好的错误信息 #
tsx
export function ErrorBoundary() {
const error = useRouteError();
const getErrorMessage = (error: unknown): string => {
if (isRouteErrorResponse(error)) {
switch (error.status) {
case 404:
return "您访问的页面不存在";
case 401:
return "请先登录后再访问";
case 403:
return "您没有权限访问此页面";
case 500:
return "服务器发生错误,请稍后重试";
default:
return "发生未知错误";
}
}
if (error instanceof Error) {
return process.env.NODE_ENV === "development"
? error.message
: "发生错误,请稍后重试";
}
return "发生未知错误";
};
return (
<div>
<h1>出错了</h1>
<p>{getErrorMessage(error)}</p>
</div>
);
}
8.2 错误日志 #
tsx
import { useEffect } from "react";
import { useRouteError } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
useEffect(() => {
// 发送错误到日志服务
logError(error);
}, [error]);
return (
<div>
<h1>出错了</h1>
<p>我们已记录此错误</p>
</div>
);
}
九、总结 #
本章我们学习了:
- ErrorBoundary:定义错误边界组件
- useRouteError:获取错误信息
- 抛出错误:在loader和action中抛出
- 嵌套错误边界:层级错误处理
- 错误恢复:重试和导航
核心要点:
- 每个路由都可以有自己的错误边界
- 使用 useRouteError 获取错误详情
- 错误会向上冒泡直到被捕获
- 提供友好的错误信息和恢复选项
最后更新:2026-03-28