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
相关产品推荐
相关产品推荐

