Remix错误处理最佳实践 #
一、错误分类 #
1.1 错误类型 #
| 类型 | HTTP状态码 | 说明 |
|---|---|---|
| 客户端错误 | 400 | 请求参数错误 |
| 未授权 | 401 | 需要登录 |
| 禁止访问 | 403 | 无权限 |
| 未找到 | 404 | 资源不存在 |
| 验证错误 | 422 | 表单验证失败 |
| 服务器错误 | 500 | 服务器内部错误 |
1.2 错误基类 #
tsx
export class AppError extends Error {
constructor(
message: string,
public status: number = 500,
public code?: string
) {
super(message);
this.name = "AppError";
}
}
export class NotFoundError extends AppError {
constructor(message: string = "资源不存在") {
super(message, 404, "NOT_FOUND");
this.name = "NotFoundError";
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = "请先登录") {
super(message, 401, "UNAUTHORIZED");
this.name = "UnauthorizedError";
}
}
export class ForbiddenError extends AppError {
constructor(message: string = "禁止访问") {
super(message, 403, "FORBIDDEN");
this.name = "ForbiddenError";
}
}
export class ValidationError extends AppError {
constructor(
public errors: Record<string, string[]>,
message: string = "验证失败"
) {
super(message, 422, "VALIDATION_ERROR");
this.name = "ValidationError";
}
}
二、统一错误处理 #
2.1 错误处理工具 #
tsx
import { json } from "@remix-run/node";
export function handleError(error: unknown) {
console.error(error);
if (error instanceof AppError) {
return json(
{
error: error.message,
code: error.code,
errors: error instanceof ValidationError ? error.errors : undefined
},
{ status: error.status }
);
}
if (error instanceof Response) {
return error;
}
return json(
{ error: "服务器内部错误", code: "INTERNAL_ERROR" },
{ status: 500 }
);
}
2.2 在loader中使用 #
tsx
import type { LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
export async function loader({ params }: LoaderFunctionArgs) {
try {
const post = await getPost(params.id);
if (!post) {
throw new NotFoundError("文章不存在");
}
return json({ post });
} catch (error) {
return handleError(error);
}
}
三、错误日志 #
3.1 日志服务 #
tsx
interface ErrorLog {
message: string;
stack?: string;
url: string;
userId?: string;
timestamp: Date;
userAgent?: string;
}
export async function logError(error: unknown, request: Request) {
const errorLog: ErrorLog = {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
url: request.url,
timestamp: new Date(),
userAgent: request.headers.get("User-Agent") || undefined,
};
// 发送到日志服务
if (process.env.NODE_ENV === "production") {
await sendToLogService(errorLog);
} else {
console.error(errorLog);
}
}
3.2 在ErrorBoundary中记录 #
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>
);
}
四、用户体验 #
4.1 友好的错误页面 #
tsx
import { Link, useRouteError, isRouteErrorResponse } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
let title = "出错了";
let message = "发生未知错误";
let action = <Link to="/">返回首页</Link>;
if (isRouteErrorResponse(error)) {
switch (error.status) {
case 404:
title = "页面不存在";
message = "您访问的页面可能已被移除或暂时不可用";
break;
case 401:
title = "请先登录";
message = "您需要登录后才能访问此页面";
action = <Link to="/login">前往登录</Link>;
break;
case 403:
title = "禁止访问";
message = "您没有权限访问此页面";
break;
case 500:
title = "服务器错误";
message = "服务器发生错误,请稍后重试";
break;
}
}
return (
<div className="error-page">
<div className="error-content">
<h1>{title}</h1>
<p>{message}</p>
<div className="error-actions">
{action}
<button onClick={() => window.location.reload()}>
刷新页面
</button>
</div>
</div>
</div>
);
}
4.2 错误恢复策略 #
tsx
import { useRevalidator, useNavigate, Link } from "@remix-run/react";
export function ErrorBoundary() {
const error = useRouteError();
const revalidator = useRevalidator();
const navigate = useNavigate();
const handleRetry = () => {
revalidator.revalidate();
};
const handleGoBack = () => {
navigate(-1);
};
return (
<div className="error-page">
<h1>出错了</h1>
<p>{error.message}</p>
<div className="actions">
<button onClick={handleRetry}>重试</button>
<button onClick={handleGoBack}>返回上一页</button>
<Link to="/">返回首页</Link>
</div>
</div>
);
}
五、表单错误处理 #
5.1 统一验证错误 #
tsx
import { z } from "zod";
import { json } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) {
throw new ValidationError(result.error.flatten().fieldErrors);
}
try {
const item = await createItem(result.data);
return redirect(`/items/${item.id}`);
} catch (error) {
return handleError(error);
}
}
5.2 显示表单错误 #
tsx
import { useActionData, Form } from "@remix-run/react";
export default function NewItem() {
const actionData = useActionData<typeof action>();
return (
<Form method="post">
{actionData?.code === "VALIDATION_ERROR" && (
<div className="alert alert-error">
请修正以下错误后重试
</div>
)}
<div>
<label>标题</label>
<input name="title" />
{actionData?.errors?.title && (
<p className="error">{actionData.errors.title.join(", ")}</p>
)}
</div>
<button type="submit">提交</button>
</Form>
);
}
六、API错误处理 #
6.1 API错误响应 #
tsx
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
if (url.searchParams.get("format") === "json") {
try {
const data = await fetchData();
return json({ success: true, data });
} catch (error) {
return json(
{ success: false, error: "获取数据失败" },
{ status: 500 }
);
}
}
// HTML响应
// ...
}
6.2 客户端错误处理 #
tsx
import { useFetcher } from "@remix-run/react";
export function useApiAction(action: string) {
const fetcher = useFetcher();
const submit = async (data: Record<string, any>) => {
fetcher.submit(data, {
method: "post",
action,
encType: "application/json",
});
};
return {
submit,
data: fetcher.data,
error: fetcher.data?.success === false ? fetcher.data.error : null,
isLoading: fetcher.state !== "idle",
};
}
七、最佳实践总结 #
7.1 错误处理清单 #
- [ ] 定义统一的错误类型
- [ ] 实现错误日志记录
- [ ] 提供友好的错误信息
- [ ] 支持错误恢复操作
- [ ] 区分开发和生产环境
7.2 代码组织 #
text
app/
├── errors/
│ ├── classes.ts # 错误类定义
│ ├── handler.ts # 错误处理函数
│ └── logger.ts # 错误日志
├── components/
│ └── ErrorBoundary.tsx
└── routes/
└── *.tsx # 各路由的ErrorBoundary
八、总结 #
本章我们学习了:
- 错误分类:定义统一的错误类型
- 统一处理:集中处理错误
- 错误日志:记录错误信息
- 用户体验:友好的错误页面
- 恢复策略:提供恢复选项
核心要点:
- 使用统一的错误类型
- 记录所有错误
- 提供友好的错误信息
- 支持错误恢复
最后更新:2026-03-28