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>
  );
}

九、总结 #

本章我们学习了:

  1. ErrorBoundary:定义错误边界组件
  2. useRouteError:获取错误信息
  3. 抛出错误:在loader和action中抛出
  4. 嵌套错误边界:层级错误处理
  5. 错误恢复:重试和导航

核心要点:

  • 每个路由都可以有自己的错误边界
  • 使用 useRouteError 获取错误详情
  • 错误会向上冒泡直到被捕获
  • 提供友好的错误信息和恢复选项
最后更新:2026-03-28