You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js v14.0.3 App Router中如何向用户展示SSR错误

Next.js 14 App Router SSR API错误提示实现方案

1. 定义安全的Result类型

先创建通用的Result类型,区分成功/失败状态,仅包含可暴露给用户的错误信息(状态码、友好提示),隔离敏感数据:

// lib/result.ts
type SuccessResult<T> = {
  success: true;
  data: T;
};

type ErrorResult = {
  success: false;
  error: {
    statusCode: number;
    userMessage: string;
    // 内部日志用的详情,不传给前端
    internalMessage?: string;
  };
};

export type ApiResult<T> = SuccessResult<T> | ErrorResult;

// 辅助生成函数
export const success = <T>(data: T): ApiResult<T> => ({ success: true, data });
export const error = (statusCode: number, userMessage: string, internalMessage?: string): ApiResult<never> => ({
  success: false,
  error: { statusCode, userMessage, internalMessage }
});

2. 统一封装API调用逻辑

把所有SSR场景的API调用封装到统一层,在这里集中处理错误、映射用户提示,避免每个路由重复写try-catch:

// lib/api.ts
import { ApiResult, success, error } from './result';

export async function fetchUserProfile(userId: string): Promise<ApiResult<{ name: string; email: string }>> {
  try {
    const res = await fetch(`https://your-api.com/users/${userId}`, {
      method: 'GET',
      headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
      cache: 'no-store'
    });

    if (!res.ok) {
      switch (res.status) {
        case 401:
          return error(401, '请登录后再访问');
        case 403:
          return error(403, '您无权访问该内容');
        case 404:
          return error(404, '请求的资源不存在');
        default:
          return error(res.status, '服务器请求失败,请稍后重试');
      }
    }

    const data = await res.json();
    return success(data);
  } catch (err) {
    console.error('Fetch user profile failed:', err);
    return error(500, '服务器内部错误,请稍后重试', (err as Error).message);
  }
}

3. 在Server Component中处理Result并渲染

在App Router的Server Component里调用封装后的API,根据Result状态直接渲染对应内容,或传递错误信息给Client Component做交互处理:

方式1:Server Component直接渲染错误提示

// app/profile/[userId]/page.tsx
import { fetchUserProfile } from '@/lib/api';

export default async function UserProfilePage({ params }: { params: { userId: string } }) {
  const result = await fetchUserProfile(params.userId);

  if (!result.success) {
    return (
      <div className="error-container">
        <h2>错误 {result.error.statusCode}</h2>
        <p>{result.error.userMessage}</p>
        {/* 仅开发环境展示内部错误详情 */}
        {process.env.NODE_ENV === 'development' && result.error.internalMessage && (
          <p className="text-sm text-gray-500">{result.error.internalMessage}</p>
        )}
      </div>
    );
  }

  return (
    <div>
      <h1>{result.data.name}</h1>
      <p>{result.data.email}</p>
    </div>
  );
}

方式2:传递错误给Client Component实现交互

如果需要跳转登录页、重试按钮等交互,可将错误信息作为props传给Client Component,或用全局Context管理错误状态:

全局错误Context实现

// app/providers.tsx
'use client';

import { createContext, useContext, useState, ReactNode, useEffect } from 'react';

type ErrorContextType = {
  currentError: { statusCode: number; message: string } | null;
  setCurrentError: (error: { statusCode: number; message: string } | null) => void;
};

const ErrorContext = createContext<ErrorContextType | undefined>(undefined);

export function ErrorProvider({ children }: { children: ReactNode }) {
  const [currentError, setCurrentError] = useState<{ statusCode: number; message: string } | null>(null);

  return (
    <ErrorContext.Provider value={{ currentError, setCurrentError }}>
      {children}
    </ErrorContext.Provider>
  );
}

export function useError() {
  const context = useContext(ErrorContext);
  if (!context) throw new Error('useError must be used within ErrorProvider');
  return context;
}

在Root Layout中注入Provider:

// app/layout.tsx
import { ErrorProvider } from './providers';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        <ErrorProvider>{children}</ErrorProvider>
      </body>
    </html>
  );
}

Server Component传递错误,Client Component处理交互:

// app/profile/[userId]/page.tsx
import { fetchUserProfile } from '@/lib/api';
import ErrorDisplay from './error-display';

export default async function UserProfilePage({ params }: { params: { userId: string } }) {
  const result = await fetchUserProfile(params.userId);

  if (!result.success) {
    return <ErrorDisplay statusCode={result.error.statusCode} message={result.error.userMessage} />;
  }

  return (
    <div>
      <h1>{result.data.name}</h1>
      <p>{result.data.email}</p>
    </div>
  );
}
// app/profile/[userId]/error-display.tsx
'use client';

import { useError } from '@/app/providers';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export default function ErrorDisplay({ statusCode, message }: { statusCode: number; message: string }) {
  const { setCurrentError } = useError();
  const router = useRouter();

  useEffect(() => {
    setCurrentError({ statusCode, message });
    return () => setCurrentError(null);
  }, [statusCode, message, setCurrentError]);

  const handleLoginRedirect = () => {
    router.push(`/login?redirect=${encodeURIComponent(window.location.pathname)}`);
  };

  return (
    <div className="error-container">
      <h2>错误 {statusCode}</h2>
      <p>{message}</p>
      {statusCode === 401 && (
        <button onClick={handleLoginRedirect} className="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
          前往登录
        </button>
      )}
      {statusCode === 500 && (
        <button onClick={() => router.refresh()} className="mt-4 px-4 py-2 bg-gray-500 text-white rounded">
          重试
        </button>
      )}
    </div>
  );
}

关键注意事项

  • 敏感信息隔离:userMessage仅包含非敏感提示,内部错误细节仅在服务端日志记录,绝不传递给前端。
  • 统一错误逻辑:所有SSR API调用通过封装函数执行,避免重复代码,方便后续统一修改提示文案。
  • 环境区分:开发环境可展示内部错误详情辅助调试,生产环境必须隐藏。

内容的提问来源于stack exchange,提问作者Specy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 10:02:03