Skip to content

路由错误处理

概述

React Router v6提供了强大的错误处理机制,允许在路由层面捕获和处理各种错误。通过errorElement和useRouteError等API,可以创建优雅的错误边界,提供良好的错误恢复体验。本文深入探讨路由错误处理的各种模式和最佳实践。

基础错误处理

errorElement基础

jsx
import { createBrowserRouter, useRouteError, isRouteErrorResponse } from 'react-router-dom';

// 基础错误边界组件
function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    // 这是由loader或action主动抛出的Response错误
    return (
      <div className="error-page">
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  // 未预期的错误
  return (
    <div className="error-page">
      <h1>Oops! Something went wrong</h1>
      <p>{error.message || 'Unknown error occurred'}</p>
      {process.env.NODE_ENV === 'development' && (
        <pre className="error-stack">{error.stack}</pre>
      )}
    </div>
  );
}

// 路由配置with错误处理
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorBoundary />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'users/:userId',
        element: <UserProfile />,
        loader: userLoader,
        errorElement: <UserErrorBoundary />
      }
    ]
  }
]);

// Loader中抛出错误
async function userLoader({ params }) {
  const { userId } = params;
  
  const response = await fetch(`/api/users/${userId}`);
  
  if (response.status === 404) {
    throw new Response('User not found', { status: 404 });
  }
  
  if (response.status === 403) {
    throw new Response('Access forbidden', { status: 403 });
  }
  
  if (!response.ok) {
    throw new Response('Failed to load user', { status: 500 });
  }
  
  return response.json();
}

// 用户特定的错误边界
function UserErrorBoundary() {
  const error = useRouteError();
  const navigate = useNavigate();

  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return (
        <div className="error-page">
          <h1>User Not Found</h1>
          <p>The user you're looking for doesn't exist.</p>
          <button onClick={() => navigate('/users')}>
            View All Users
          </button>
        </div>
      );
    }

    if (error.status === 403) {
      return (
        <div className="error-page">
          <h1>Access Denied</h1>
          <p>You don't have permission to view this user's profile.</p>
          <button onClick={() => navigate('/')}>
            Go Home
          </button>
        </div>
      );
    }
  }

  return (
    <div className="error-page">
      <h1>Error Loading User</h1>
      <p>Something went wrong while loading the user profile.</p>
      <button onClick={() => navigate(0)}>
        Try Again
      </button>
      <button onClick={() => navigate('/users')}>
        Back to Users
      </button>
    </div>
  );
}

嵌套错误边界

jsx
// 多层错误边界
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <RootErrorBoundary />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'dashboard',
        element: <DashboardLayout />,
        errorElement: <DashboardErrorBoundary />,
        children: [
          {
            index: true,
            element: <DashboardHome />
          },
          {
            path: 'analytics',
            element: <Analytics />,
            loader: analyticsLoader,
            errorElement: <AnalyticsErrorBoundary />
          },
          {
            path: 'settings',
            element: <Settings />,
            loader: settingsLoader,
            errorElement: <SettingsErrorBoundary />
          }
        ]
      }
    ]
  }
]);

// 根级错误边界 - 处理最严重的错误
function RootErrorBoundary() {
  const error = useRouteError();

  return (
    <div className="root-error">
      <div className="error-container">
        <h1>Application Error</h1>
        <p>We're sorry, but something went wrong with the application.</p>
        
        {isRouteErrorResponse(error) && (
          <div className="error-details">
            <p>Status: {error.status}</p>
            <p>Message: {error.statusText}</p>
          </div>
        )}

        <div className="error-actions">
          <button onClick={() => window.location.href = '/'}>
            Return to Home
          </button>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>

        {process.env.NODE_ENV === 'development' && error.stack && (
          <details className="error-stack">
            <summary>Error Stack Trace</summary>
            <pre>{error.stack}</pre>
          </details>
        )}
      </div>
    </div>
  );
}

// 仪表板错误边界 - 保留导航
function DashboardErrorBoundary() {
  const error = useRouteError();

  return (
    <div className="dashboard-error">
      <aside className="dashboard-sidebar">
        <DashboardNavigation />
      </aside>
      
      <main className="dashboard-content">
        <div className="error-message">
          <h2>Dashboard Error</h2>
          <p>An error occurred while loading this section of the dashboard.</p>
          
          {isRouteErrorResponse(error) && (
            <p className="error-detail">{error.data}</p>
          )}
          
          <div className="error-actions">
            <Link to="/dashboard">Go to Dashboard Home</Link>
            <button onClick={() => window.location.reload()}>
              Try Again
            </button>
          </div>
        </div>
      </main>
    </div>
  );
}

// 分析页面错误边界 - 最具体的错误处理
function AnalyticsErrorBoundary() {
  const error = useRouteError();
  const navigate = useNavigate();

  return (
    <div className="analytics-error">
      <h2>Analytics Error</h2>
      <p>Unable to load analytics data.</p>
      
      {isRouteErrorResponse(error) && error.status === 403 && (
        <div className="permission-error">
          <p>You don't have permission to view analytics.</p>
          <Link to="/dashboard/settings">
            Request Access
          </Link>
        </div>
      )}
      
      {isRouteErrorResponse(error) && error.status === 500 && (
        <div className="server-error">
          <p>Our analytics service is temporarily unavailable.</p>
          <button onClick={() => navigate(0)}>Retry</button>
        </div>
      )}
      
      <Link to="/dashboard">Back to Dashboard</Link>
    </div>
  );
}

错误类型处理

HTTP状态码错误

jsx
// 统一的HTTP错误处理
class HTTPError extends Error {
  constructor(status, statusText, data) {
    super(statusText);
    this.status = status;
    this.statusText = statusText;
    this.data = data;
  }
}

// Loader中创建特定错误
async function resourceLoader({ params }) {
  const response = await fetch(`/api/resources/${params.id}`);

  switch (response.status) {
    case 404:
      throw new Response('Resource not found', {
        status: 404,
        statusText: 'Not Found'
      });

    case 401:
      throw new Response('Please log in to continue', {
        status: 401,
        statusText: 'Unauthorized'
      });

    case 403:
      throw new Response('You do not have access to this resource', {
        status: 403,
        statusText: 'Forbidden'
      });

    case 429:
      throw new Response('Too many requests. Please try again later.', {
        status: 429,
        statusText: 'Too Many Requests'
      });

    case 503:
      throw new Response('Service temporarily unavailable', {
        status: 503,
        statusText: 'Service Unavailable'
      });

    default:
      if (!response.ok) {
        throw new Response('An unexpected error occurred', {
          status: response.status,
          statusText: 'Server Error'
        });
      }
  }

  return response.json();
}

// 状态码特定的错误处理
function HTTPErrorBoundary() {
  const error = useRouteError();
  const navigate = useNavigate();
  const location = useLocation();

  if (!isRouteErrorResponse(error)) {
    return <GenericErrorPage error={error} />;
  }

  const errorHandlers = {
    404: () => (
      <div className="error-404">
        <h1>404 - Page Not Found</h1>
        <p>The page you're looking for doesn't exist.</p>
        <div className="error-suggestions">
          <h3>You might want to:</h3>
          <ul>
            <li><Link to="/">Go to homepage</Link></li>
            <li><button onClick={() => navigate(-1)}>Go back</button></li>
            <li><Link to="/search">Search our site</Link></li>
          </ul>
        </div>
      </div>
    ),

    401: () => (
      <div className="error-401">
        <h1>Authentication Required</h1>
        <p>{error.data}</p>
        <button onClick={() => {
          navigate('/login', {
            state: { from: location.pathname }
          });
        }}>
          Sign In
        </button>
      </div>
    ),

    403: () => (
      <div className="error-403">
        <h1>Access Denied</h1>
        <p>{error.data}</p>
        <div className="error-actions">
          <Link to="/">Go Home</Link>
          <Link to="/contact">Contact Support</Link>
        </div>
      </div>
    ),

    429: () => (
      <div className="error-429">
        <h1>Too Many Requests</h1>
        <p>You've made too many requests. Please wait a moment before trying again.</p>
        <RateLimitTimer onExpire={() => navigate(0)} />
      </div>
    ),

    500: () => (
      <div className="error-500">
        <h1>Server Error</h1>
        <p>Something went wrong on our end. We're working to fix it.</p>
        <button onClick={() => navigate(0)}>Try Again</button>
      </div>
    ),

    503: () => (
      <div className="error-503">
        <h1>Service Unavailable</h1>
        <p>Our service is temporarily down for maintenance.</p>
        <p>Please check back in a few minutes.</p>
        <ServiceStatusChecker />
      </div>
    )
  };

  const ErrorComponent = errorHandlers[error.status] || (() => (
    <div className="error-generic">
      <h1>{error.status} - {error.statusText}</h1>
      <p>{error.data}</p>
      <button onClick={() => navigate(-1)}>Go Back</button>
    </div>
  ));

  return <ErrorComponent />;
}

// 速率限制计时器组件
function RateLimitTimer({ onExpire }) {
  const [remaining, setRemaining] = useState(60);

  useEffect(() => {
    const timer = setInterval(() => {
      setRemaining(prev => {
        if (prev <= 1) {
          clearInterval(timer);
          onExpire();
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    return () => clearInterval(timer);
  }, [onExpire]);

  return (
    <div className="rate-limit-timer">
      <p>You can try again in {remaining} seconds</p>
    </div>
  );
}

// 服务状态检查器
function ServiceStatusChecker() {
  const [status, setStatus] = useState('checking');

  useEffect(() => {
    const checkStatus = async () => {
      try {
        const response = await fetch('/api/health');
        if (response.ok) {
          setStatus('available');
        } else {
          setStatus('unavailable');
        }
      } catch {
        setStatus('unavailable');
      }
    };

    const interval = setInterval(checkStatus, 10000); // 每10秒检查一次
    checkStatus();

    return () => clearInterval(interval);
  }, []);

  if (status === 'available') {
    return (
      <div className="service-available">
        <p>Service is back online!</p>
        <button onClick={() => window.location.reload()}>
          Refresh Page
        </button>
      </div>
    );
  }

  return (
    <div className="service-checking">
      <p>Checking service status...</p>
    </div>
  );
}

网络错误处理

jsx
// 网络错误检测和处理
async function networkAwareLoader({ params }) {
  try {
    const response = await fetch(`/api/data/${params.id}`);
    
    if (!response.ok) {
      throw new Response('Failed to fetch data', {
        status: response.status
      });
    }
    
    return response.json();
    
  } catch (error) {
    // 网络错误
    if (error instanceof TypeError && error.message.includes('fetch')) {
      throw new Response('Network error. Please check your internet connection.', {
        status: 0, // 特殊状态码表示网络错误
        statusText: 'Network Error'
      });
    }
    
    // 超时错误
    if (error.name === 'AbortError') {
      throw new Response('Request timed out. Please try again.', {
        status: 0,
        statusText: 'Timeout'
      });
    }
    
    throw error;
  }
}

// 网络错误边界
function NetworkErrorBoundary() {
  const error = useRouteError();
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  const navigate = useNavigate();

  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  if (isRouteErrorResponse(error) && error.status === 0) {
    return (
      <div className="network-error">
        <div className="error-icon">
          {isOnline ? '⚠️' : '📡'}
        </div>
        
        <h1>
          {isOnline ? 'Connection Problem' : 'You are offline'}
        </h1>
        
        <p>
          {isOnline 
            ? 'Unable to connect to the server. Please check your connection and try again.'
            : 'Please check your internet connection and try again.'
          }
        </p>

        <div className="connection-status">
          <div className={`status-indicator ${isOnline ? 'online' : 'offline'}`} />
          <span>{isOnline ? 'Connected' : 'No connection'}</span>
        </div>

        <div className="error-actions">
          <button onClick={() => navigate(0)} disabled={!isOnline}>
            Try Again
          </button>
          <button onClick={() => navigate(-1)}>
            Go Back
          </button>
        </div>

        <NetworkDiagnostics />
      </div>
    );
  }

  return <DefaultErrorBoundary />;
}

// 网络诊断组件
function NetworkDiagnostics() {
  const [diagnostics, setDiagnostics] = useState(null);

  const runDiagnostics = async () => {
    const results = {
      onlineStatus: navigator.onLine,
      connectionType: navigator.connection?.effectiveType || 'unknown',
      downlink: navigator.connection?.downlink || 'unknown',
      rtt: navigator.connection?.rtt || 'unknown'
    };

    // 测试DNS
    try {
      await fetch('https://dns.google/resolve?name=example.com', {
        mode: 'no-cors'
      });
      results.dns = 'OK';
    } catch {
      results.dns = 'Failed';
    }

    // 测试API连接
    try {
      await fetch('/api/health', {
        method: 'HEAD'
      });
      results.api = 'OK';
    } catch {
      results.api = 'Failed';
    }

    setDiagnostics(results);
  };

  return (
    <div className="network-diagnostics">
      <button onClick={runDiagnostics}>
        Run Network Diagnostics
      </button>

      {diagnostics && (
        <div className="diagnostics-results">
          <h3>Diagnostics Results:</h3>
          <ul>
            <li>Online Status: {diagnostics.onlineStatus ? 'Online' : 'Offline'}</li>
            <li>Connection Type: {diagnostics.connectionType}</li>
            <li>Download Speed: {diagnostics.downlink} Mbps</li>
            <li>Latency: {diagnostics.rtt} ms</li>
            <li>DNS: {diagnostics.dns}</li>
            <li>API Server: {diagnostics.api}</li>
          </ul>
        </div>
      )}
    </div>
  );
}

权限错误处理

jsx
// 权限错误类
class PermissionError extends Error {
  constructor(message, requiredPermission, userPermissions) {
    super(message);
    this.name = 'PermissionError';
    this.requiredPermission = requiredPermission;
    this.userPermissions = userPermissions;
  }
}

// 带权限检查的Loader
async function protectedLoader({ params, context }) {
  const { user, permissions } = context;

  // 检查认证
  if (!user) {
    throw new Response('Authentication required', {
      status: 401,
      statusText: 'Unauthorized'
    });
  }

  // 检查权限
  const requiredPermission = 'resource:read';
  if (!permissions.includes(requiredPermission)) {
    throw new PermissionError(
      'You do not have permission to view this resource',
      requiredPermission,
      permissions
    );
  }

  // 加载数据
  const data = await fetch(`/api/resources/${params.id}`)
    .then(r => r.json());

  return data;
}

// 权限错误边界
function PermissionErrorBoundary() {
  const error = useRouteError();
  const { user } = useAuth();
  const navigate = useNavigate();

  // 处理PermissionError
  if (error instanceof PermissionError) {
    return (
      <div className="permission-error">
        <h1>Access Denied</h1>
        <p>{error.message}</p>

        <div className="permission-details">
          <h3>Permission Required:</h3>
          <code>{error.requiredPermission}</code>

          <h3>Your Permissions:</h3>
          <ul>
            {error.userPermissions.map(perm => (
              <li key={perm}><code>{perm}</code></li>
            ))}
          </ul>
        </div>

        <div className="error-actions">
          <button onClick={() => navigate('/')}>
            Go Home
          </button>
          <Link to="/upgrade">
            Upgrade Your Account
          </Link>
          <Link to="/contact">
            Request Access
          </Link>
        </div>
      </div>
    );
  }

  // 处理401错误
  if (isRouteErrorResponse(error) && error.status === 401) {
    return (
      <div className="auth-required">
        <h1>Sign In Required</h1>
        <p>Please sign in to access this page.</p>
        
        <button onClick={() => {
          navigate('/login', {
            state: { from: location.pathname }
          });
        }}>
          Sign In
        </button>

        <p>
          Don't have an account? <Link to="/register">Sign up</Link>
        </p>
      </div>
    );
  }

  return <DefaultErrorBoundary />;
}

错误恢复策略

自动重试

jsx
// 带重试的Loader
async function retryableLoader({ params }, retryCount = 0) {
  const MAX_RETRIES = 3;
  const RETRY_DELAY = 1000;

  try {
    const response = await fetch(`/api/data/${params.id}`);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    
    return response.json();
    
  } catch (error) {
    if (retryCount < MAX_RETRIES) {
      console.log(`Retry attempt ${retryCount + 1} of ${MAX_RETRIES}`);
      
      // 指数退避
      const delay = RETRY_DELAY * Math.pow(2, retryCount);
      await new Promise(resolve => setTimeout(resolve, delay));
      
      return retryableLoader({ params }, retryCount + 1);
    }
    
    // 达到最大重试次数
    throw new Response(
      `Failed to load data after ${MAX_RETRIES} attempts`,
      { status: 500 }
    );
  }
}

// 带重试UI的错误边界
function RetryableErrorBoundary() {
  const error = useRouteError();
  const navigate = useNavigate();
  const [retrying, setRetrying] = useState(false);
  const [retryCount, setRetryCount] = useState(0);

  const handleRetry = async () => {
    setRetrying(true);
    setRetryCount(prev => prev + 1);

    // 延迟后重试
    await new Promise(resolve => setTimeout(resolve, 1000));
    
    navigate(0); // 重新加载当前路由
  };

  return (
    <div className="retryable-error">
      <h1>Something went wrong</h1>
      <p>{error.data || error.message}</p>

      {retryCount > 0 && (
        <p className="retry-info">
          Retry attempts: {retryCount}
        </p>
      )}

      <div className="error-actions">
        <button 
          onClick={handleRetry} 
          disabled={retrying}
        >
          {retrying ? 'Retrying...' : 'Try Again'}
        </button>

        <button onClick={() => navigate(-1)}>
          Go Back
        </button>

        {retryCount >= 3 && (
          <Link to="/support">
            Contact Support
          </Link>
        )}
      </div>

      {retrying && (
        <div className="retry-progress">
          <div className="spinner" />
          <p>Retrying...</p>
        </div>
      )}
    </div>
  );
}

降级处理

jsx
// 带降级的Loader
async function fallbackLoader({ params }) {
  try {
    // 尝试从主API加载
    const response = await fetch(`/api/v2/data/${params.id}`);
    
    if (!response.ok) {
      throw new Error('Primary API failed');
    }
    
    return {
      data: await response.json(),
      source: 'primary'
    };
    
  } catch (primaryError) {
    console.warn('Primary API failed, trying fallback...', primaryError);
    
    try {
      // 尝试从备用API加载
      const fallbackResponse = await fetch(`/api/v1/data/${params.id}`);
      
      if (!fallbackResponse.ok) {
        throw new Error('Fallback API failed');
      }
      
      return {
        data: await fallbackResponse.json(),
        source: 'fallback',
        warning: 'Using older API version'
      };
      
    } catch (fallbackError) {
      console.error('Both APIs failed', fallbackError);
      
      // 尝试从缓存加载
      const cached = getCachedData(params.id);
      if (cached) {
        return {
          data: cached,
          source: 'cache',
          warning: 'Showing cached data - may be outdated'
        };
      }
      
      // 所有选项都失败了
      throw new Response(
        'Unable to load data from any source',
        { status: 503 }
      );
    }
  }
}

// 显示降级警告的组件
function DataView() {
  const loaderData = useLoaderData();
  const { data, source, warning } = loaderData;

  return (
    <div className="data-view">
      {warning && (
        <div className="degraded-service-warning">
          <strong>Notice:</strong> {warning}
          {source === 'cache' && (
            <button onClick={() => window.location.reload()}>
              Try to Refresh
            </button>
          )}
        </div>
      )}

      <div className="data-content">
        {/* 渲染数据 */}
        <pre>{JSON.stringify(data, null, 2)}</pre>
      </div>

      {source !== 'primary' && (
        <div className="data-source-info">
          Data source: {source}
        </div>
      )}
    </div>
  );
}

部分失败处理

jsx
// 处理部分数据加载失败
async function partialFailureLoader({ params }) {
  const results = {
    critical: null,
    optional: [],
    errors: []
  };

  // 关键数据必须成功
  try {
    results.critical = await fetch(`/api/critical/${params.id}`)
      .then(r => r.json());
  } catch (error) {
    throw new Response('Failed to load critical data', { status: 500 });
  }

  // 可选数据允许失败
  const optionalPromises = [
    fetch(`/api/optional1/${params.id}`).then(r => r.json()),
    fetch(`/api/optional2/${params.id}`).then(r => r.json()),
    fetch(`/api/optional3/${params.id}`).then(r => r.json())
  ];

  const optionalResults = await Promise.allSettled(optionalPromises);

  optionalResults.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      results.optional.push(result.value);
    } else {
      results.errors.push({
        source: `optional${index + 1}`,
        error: result.reason.message
      });
    }
  });

  return results;
}

// 显示部分失败的组件
function PartialDataView() {
  const { critical, optional, errors } = useLoaderData();

  return (
    <div className="partial-data-view">
      {/* 关键数据总是显示 */}
      <section className="critical-section">
        <h2>Main Content</h2>
        <div>{critical.content}</div>
      </section>

      {/* 可选数据 */}
      {optional.length > 0 && (
        <section className="optional-section">
          <h2>Additional Information</h2>
          {optional.map((item, index) => (
            <div key={index}>{item.content}</div>
          ))}
        </section>
      )}

      {/* 错误提示 */}
      {errors.length > 0 && (
        <div className="partial-errors">
          <p>Some content could not be loaded:</p>
          <ul>
            {errors.map((error, index) => (
              <li key={index}>
                {error.source}: {error.error}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

错误报告和监控

错误日志记录

jsx
// 错误日志服务
class ErrorLogger {
  constructor() {
    this.logs = [];
    this.maxLogs = 100;
  }

  log(error, context = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      error: {
        message: error.message,
        stack: error.stack,
        name: error.name
      },
      context: {
        url: window.location.href,
        userAgent: navigator.userAgent,
        ...context
      }
    };

    this.logs.push(logEntry);

    // 限制日志数量
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }

    // 发送到服务器
    this.sendToServer(logEntry);

    return logEntry;
  }

  async sendToServer(logEntry) {
    try {
      await fetch('/api/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(logEntry)
      });
    } catch (error) {
      console.error('Failed to send error log:', error);
    }
  }

  getLogs() {
    return [...this.logs];
  }

  clearLogs() {
    this.logs = [];
  }
}

const errorLogger = new ErrorLogger();

// 带日志的错误边界
function LoggingErrorBoundary() {
  const error = useRouteError();
  const location = useLocation();
  const { user } = useAuth();

  useEffect(() => {
    // 记录错误
    errorLogger.log(error, {
      route: location.pathname,
      userId: user?.id,
      userRole: user?.role,
      isRouteError: isRouteErrorResponse(error),
      status: isRouteErrorResponse(error) ? error.status : undefined
    });
  }, [error, location, user]);

  return (
    <div className="error-page">
      <h1>Something went wrong</h1>
      <p>We've been notified and are working on a fix.</p>

      {process.env.NODE_ENV === 'development' && (
        <details className="error-details">
          <summary>Error Details</summary>
          <pre>{JSON.stringify({
            message: error.message,
            stack: error.stack,
            location: location.pathname
          }, null, 2)}</pre>
        </details>
      )}

      <button onClick={() => window.location.reload()}>
        Try Again
      </button>
    </div>
  );
}

集成第三方错误监控

jsx
// Sentry集成
import * as Sentry from '@sentry/react';

// 初始化Sentry
Sentry.init({
  dsn: process.env.REACT_APP_SENTRY_DSN,
  environment: process.env.NODE_ENV,
  beforeSend(event, hint) {
    // 过滤敏感信息
    if (event.user) {
      delete event.user.email;
      delete event.user.ip_address;
    }
    return event;
  }
});

// Sentry错误边界
function SentryErrorBoundary() {
  const error = useRouteError();
  const location = useLocation();

  useEffect(() => {
    // 发送错误到Sentry
    Sentry.captureException(error, {
      contexts: {
        router: {
          location: location.pathname,
          search: location.search
        }
      },
      tags: {
        errorType: isRouteErrorResponse(error) ? 'route' : 'runtime',
        status: isRouteErrorResponse(error) ? error.status : undefined
      }
    });
  }, [error, location]);

  return (
    <div className="error-page">
      <h1>Oops! Something went wrong</h1>
      <p>Our team has been notified and is working on a fix.</p>
      
      <button onClick={() => {
        Sentry.showReportDialog({
          eventId: Sentry.lastEventId(),
          user: {
            name: 'User',
            email: ''
          }
        });
      }}>
        Report Feedback
      </button>
    </div>
  );
}

// 自定义错误监控
class ErrorMonitor {
  constructor() {
    this.errors = [];
    this.listeners = [];
  }

  capture(error, context = {}) {
    const errorData = {
      id: Date.now(),
      timestamp: new Date().toISOString(),
      error: {
        message: error.message,
        stack: error.stack,
        name: error.name
      },
      context,
      userAgent: navigator.userAgent,
      url: window.location.href
    };

    this.errors.push(errorData);
    this.notifyListeners(errorData);

    // 发送到监控服务
    this.sendToMonitoring(errorData);
  }

  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  notifyListeners(errorData) {
    this.listeners.forEach(listener => {
      try {
        listener(errorData);
      } catch (error) {
        console.error('Error in error listener:', error);
      }
    });
  }

  async sendToMonitoring(errorData) {
    try {
      await fetch('/api/monitoring/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(errorData)
      });
    } catch (error) {
      console.error('Failed to send error to monitoring:', error);
    }
  }

  getErrors() {
    return [...this.errors];
  }

  clearErrors() {
    this.errors = [];
  }
}

const errorMonitor = new ErrorMonitor();

// 错误监控Hook
function useErrorMonitoring() {
  const [errors, setErrors] = useState([]);

  useEffect(() => {
    const unsubscribe = errorMonitor.subscribe((errorData) => {
      setErrors(prev => [...prev, errorData].slice(-10)); // 保留最新10个错误
    });

    return unsubscribe;
  }, []);

  return {
    errors,
    clearErrors: () => {
      setErrors([]);
      errorMonitor.clearErrors();
    }
  };
}

// 错误监控面板
function ErrorMonitoringPanel() {
  const { errors, clearErrors } = useErrorMonitoring();

  if (process.env.NODE_ENV !== 'development') {
    return null;
  }

  return (
    <div className="error-monitoring-panel">
      <div className="panel-header">
        <h3>Error Monitor ({errors.length})</h3>
        <button onClick={clearErrors}>Clear</button>
      </div>

      <div className="errors-list">
        {errors.map(error => (
          <div key={error.id} className="error-item">
            <div className="error-time">
              {new Date(error.timestamp).toLocaleTimeString()}
            </div>
            <div className="error-message">
              {error.error.message}
            </div>
            <div className="error-location">
              {error.context.route}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

用户友好的错误页面

交互式错误页面

jsx
// 丰富的错误页面组件
function RichErrorPage() {
  const error = useRouteError();
  const navigate = useNavigate();
  const [feedback, setFeedback] = useState('');
  const [submitted, setSubmitted] = useState(false);

  const handleSubmitFeedback = async () => {
    try {
      await fetch('/api/feedback', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          error: error.message,
          feedback,
          url: window.location.href,
          timestamp: new Date().toISOString()
        })
      });

      setSubmitted(true);
      setTimeout(() => setSubmitted(false), 3000);
    } catch (error) {
      console.error('Failed to submit feedback:', error);
    }
  };

  return (
    <div className="rich-error-page">
      <div className="error-container">
        <div className="error-icon">
          <ErrorIllustration type={getErrorType(error)} />
        </div>

        <h1>Oops! Something went wrong</h1>
        
        <p className="error-message">
          {getErrorMessage(error)}
        </p>

        {/* 建议的操作 */}
        <div className="suggested-actions">
          <h3>You can try:</h3>
          <div className="actions-grid">
            <button 
              className="action-card"
              onClick={() => navigate(0)}
            >
              <span className="action-icon">🔄</span>
              <span className="action-text">Refresh the page</span>
            </button>

            <button 
              className="action-card"
              onClick={() => navigate(-1)}
            >
              <span className="action-icon">⬅️</span>
              <span className="action-text">Go back</span>
            </button>

            <button 
              className="action-card"
              onClick={() => navigate('/')}
            >
              <span className="action-icon">🏠</span>
              <span className="action-text">Go to homepage</span>
            </button>

            <Link to="/help" className="action-card">
              <span className="action-icon">❓</span>
              <span className="action-text">Visit help center</span>
            </Link>
          </div>
        </div>

        {/* 反馈表单 */}
        <div className="error-feedback">
          <h3>Help us improve</h3>
          <p>Let us know what happened:</p>
          
          <textarea
            value={feedback}
            onChange={(e) => setFeedback(e.target.value)}
            placeholder="Describe what you were trying to do..."
            rows="4"
          />

          <button 
            onClick={handleSubmitFeedback}
            disabled={!feedback || submitted}
          >
            {submitted ? 'Thank you!' : 'Send Feedback'}
          </button>
        </div>

        {/* 常见问题 */}
        <div className="error-faq">
          <h3>Common questions:</h3>
          <details>
            <summary>Why did this happen?</summary>
            <p>This error occurred because {getErrorExplanation(error)}</p>
          </details>
          
          <details>
            <summary>Will my data be lost?</summary>
            <p>No, your data is safe. We automatically save your work.</p>
          </details>
          
          <details>
            <summary>How long will this take to fix?</summary>
            <p>Our team has been notified and is working on a solution.</p>
          </details>
        </div>
      </div>
    </div>
  );
}

// 错误类型图标
function ErrorIllustration({ type }) {
  const illustrations = {
    '404': '🔍',
    '403': '🔒',
    '500': '🔧',
    'network': '📡',
    'default': '⚠️'
  };

  return (
    <div className="error-illustration">
      {illustrations[type] || illustrations.default}
    </div>
  );
}

// 获取用户友好的错误信息
function getErrorMessage(error) {
  if (isRouteErrorResponse(error)) {
    const messages = {
      404: "We couldn't find the page you're looking for.",
      403: "You don't have permission to access this page.",
      401: "Please sign in to continue.",
      500: "Something went wrong on our end.",
      503: "This service is temporarily unavailable."
    };

    return messages[error.status] || error.data;
  }

  return "An unexpected error occurred. Please try again.";
}

// 获取错误类型
function getErrorType(error) {
  if (isRouteErrorResponse(error)) {
    return error.status.toString();
  }

  if (error.message?.includes('network')) {
    return 'network';
  }

  return 'default';
}

// 获取错误解释
function getErrorExplanation(error) {
  if (isRouteErrorResponse(error)) {
    const explanations = {
      404: "the page you requested doesn't exist or has been moved.",
      403: "you don't have the necessary permissions.",
      500: "there was a problem on our server.",
      503: "our service is temporarily down for maintenance."
    };

    return explanations[error.status] || "of an unexpected issue.";
  }

  return "of an unexpected technical issue.";
}

品牌化错误页面

jsx
// 品牌化的404页面
function Custom404Page() {
  const navigate = useNavigate();
  const [searchQuery, setSearchQuery] = useState('');

  const handleSearch = (e) => {
    e.preventDefault();
    if (searchQuery.trim()) {
      navigate(`/search?q=${encodeURIComponent(searchQuery)}`);
    }
  };

  return (
    <div className="custom-404">
      <div className="error-content">
        <div className="error-animation">
          <Lost404Animation />
        </div>

        <h1>Page Not Found</h1>
        <p className="error-subtitle">
          Looks like you've ventured into uncharted territory!
        </p>

        {/* 搜索栏 */}
        <form onSubmit={handleSearch} className="error-search">
          <input
            type="text"
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            placeholder="What are you looking for?"
          />
          <button type="submit">Search</button>
        </form>

        {/* 热门链接 */}
        <div className="popular-links">
          <h3>Popular pages:</h3>
          <div className="links-grid">
            <Link to="/" className="link-card">
              <span>🏠</span>
              <span>Home</span>
            </Link>
            <Link to="/products" className="link-card">
              <span>🛍️</span>
              <span>Products</span>
            </Link>
            <Link to="/about" className="link-card">
              <span>ℹ️</span>
              <span>About</span>
            </Link>
            <Link to="/contact" className="link-card">
              <span>📧</span>
              <span>Contact</span>
            </Link>
          </div>
        </div>
      </div>
    </div>
  );
}

// 品牌化的维护页面
function MaintenancePage() {
  const [timeRemaining, setTimeRemaining] = useState(null);

  useEffect(() => {
    // 获取维护结束时间
    fetch('/api/maintenance/status')
      .then(r => r.json())
      .then(data => {
        if (data.endTime) {
          const end = new Date(data.endTime);
          updateCountdown(end);

          const interval = setInterval(() => {
            updateCountdown(end);
          }, 1000);

          return () => clearInterval(interval);
        }
      });
  }, []);

  const updateCountdown = (endTime) => {
    const now = new Date();
    const diff = endTime - now;

    if (diff <= 0) {
      window.location.reload();
      return;
    }

    const hours = Math.floor(diff / (1000 * 60 * 60));
    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
    const seconds = Math.floor((diff % (1000 * 60)) / 1000);

    setTimeRemaining({ hours, minutes, seconds });
  };

  return (
    <div className="maintenance-page">
      <div className="maintenance-content">
        <MaintenanceAnimation />

        <h1>We'll be right back!</h1>
        <p>We're making some improvements to serve you better.</p>

        {timeRemaining && (
          <div className="countdown">
            <div className="countdown-item">
              <span className="countdown-value">{timeRemaining.hours}</span>
              <span className="countdown-label">Hours</span>
            </div>
            <div className="countdown-item">
              <span className="countdown-value">{timeRemaining.minutes}</span>
              <span className="countdown-label">Minutes</span>
            </div>
            <div className="countdown-item">
              <span className="countdown-value">{timeRemaining.seconds}</span>
              <span className="countdown-label">Seconds</span>
            </div>
          </div>
        )}

        <div className="maintenance-info">
          <p>In the meantime:</p>
          <ul>
            <li>Follow us on social media for updates</li>
            <li>Check our status page for real-time information</li>
            <li>Contact support if you need immediate assistance</li>
          </ul>
        </div>

        <div className="social-links">
          <a href="https://twitter.com/yourapp" target="_blank" rel="noopener noreferrer">
            Twitter
          </a>
          <a href="https://facebook.com/yourapp" target="_blank" rel="noopener noreferrer">
            Facebook
          </a>
          <a href="https://status.yourapp.com" target="_blank" rel="noopener noreferrer">
            Status Page
          </a>
        </div>
      </div>
    </div>
  );
}

错误处理最佳实践

1. 错误边界层次设计

jsx
// 分层错误处理策略
const errorBoundaryStrategy = {
  // 全局层 - 捕获致命错误
  global: {
    purpose: '处理应用级错误,防止整个应用崩溃',
    features: ['错误日志', '用户反馈', '重启应用'],
    example: '<RootErrorBoundary />'
  },

  // 布局层 - 保留导航
  layout: {
    purpose: '处理布局内容错误,保留应用框架',
    features: ['保留导航', '错误恢复', '部分刷新'],
    example: '<LayoutErrorBoundary />'
  },

  // 功能层 - 隔离功能模块
  feature: {
    purpose: '隔离单个功能模块的错误',
    features: ['降级显示', '重试机制', '替代方案'],
    example: '<FeatureErrorBoundary />'
  },

  // 组件层 - 最细粒度
  component: {
    purpose: '处理单个组件错误,不影响其他组件',
    features: ['占位符', '默认值', '静默失败'],
    example: '<ComponentErrorBoundary />'
  }
};

2. 错误信息本地化

jsx
// 多语言错误消息
const errorMessages = {
  en: {
    404: 'Page not found',
    403: 'Access denied',
    500: 'Server error',
    network: 'Network error',
    retry: 'Try again',
    goBack: 'Go back',
    goHome: 'Go to homepage'
  },
  zh: {
    404: '页面未找到',
    403: '访问被拒绝',
    500: '服务器错误',
    network: '网络错误',
    retry: '重试',
    goBack: '返回',
    goHome: '返回首页'
  }
};

function LocalizedErrorBoundary() {
  const error = useRouteError();
  const { language } = useLanguage();

  const messages = errorMessages[language] || errorMessages.en;

  return (
    <div className="error-page">
      <h1>{messages[error.status] || messages[500]}</h1>
      <div className="error-actions">
        <button onClick={() => navigate(0)}>
          {messages.retry}
        </button>
        <button onClick={() => navigate(-1)}>
          {messages.goBack}
        </button>
        <button onClick={() => navigate('/')}>
          {messages.goHome}
        </button>
      </div>
    </div>
  );
}

3. 性能监控

jsx
// 错误性能追踪
class ErrorPerformanceTracker {
  constructor() {
    this.metrics = [];
  }

  trackError(error, context = {}) {
    const metric = {
      timestamp: performance.now(),
      error: {
        message: error.message,
        type: error.name
      },
      context,
      performance: {
        memory: performance.memory?.usedJSHeapSize,
        navigation: performance.getEntriesByType('navigation')[0]
      }
    };

    this.metrics.push(metric);

    // 发送到分析服务
    this.sendMetrics(metric);
  }

  async sendMetrics(metric) {
    try {
      await fetch('/api/metrics/errors', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(metric)
      });
    } catch (error) {
      console.error('Failed to send error metrics:', error);
    }
  }

  getMetrics() {
    return [...this.metrics];
  }
}

const errorTracker = new ErrorPerformanceTracker();

总结

React Router v6的错误处理机制提供了完善的解决方案:

  1. errorElement:路由级错误边界
  2. useRouteError:获取错误详情
  3. 嵌套处理:多层次错误隔离
  4. 错误恢复:重试、降级、部分失败处理
  5. 用户体验:友好的错误页面和交互
  6. 监控日志:完整的错误追踪和报告

合理的错误处理策略能够显著提升应用的健壮性和用户体验。