Next.js 提供了多种错误处理机制,帮助开发者构建健壮的应用。了解这些机制对于构建可靠的用户体验至关重要。
错误处理层次
Next.js 的错误处理分为多个层次:
- 组件级错误:使用 Error Boundary
- 路由级错误:使用 error.js 文件
- 服务器级错误:使用全局错误处理
- API 错误:在 API Routes 中处理
路由级错误处理(App Router)
error.js 文件
在 App Router 中,error.js 文件用于捕获路由段及其子组件中的错误。
javascript// app/error.js 'use client'; export default function Error({ error, reset, }) { return ( <div> <h2>Something went wrong!</h2> <p>{error.message}</p> <button onClick={() => reset()}>Try again</button> </div> ); } export default function GlobalError({ error, reset }) { return ( <html> <body> <h2>Something went wrong!</h2> <button onClick={reset}>Try again</button> </body> </html> ); }
嵌套错误边界
javascript// app/dashboard/error.js 'use client'; export default function DashboardError({ error, reset }) { return ( <div> <h2>Dashboard Error</h2> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> ); } // app/dashboard/settings/error.js 'use client'; export default function SettingsError({ error, reset }) { return ( <div> <h2>Settings Error</h2> <p>{error.message}</p> <button onClick={reset}>Retry</button> </div> ); }
Pages Router 错误处理
自定义错误页面
javascript// pages/404.js export default function Custom404() { return ( <div> <h1>404 - Page Not Found</h1> <p>The page you're looking for doesn't exist.</p> <Link href="/">Go Home</Link> </div> ); } // pages/500.js export default function Custom500() { return ( <div> <h1>500 - Server Error</h1> <p>Something went wrong on our end.</p> <Link href="/">Go Home</Link> </div> ); } // pages/_error.js export default function Error({ statusCode, err }) { return ( <div> <p> {statusCode ? `An error ${statusCode} occurred on server` : 'An error occurred on client'} </p> <p>{err?.message}</p> </div> ); } Error.getInitialProps = ({ res, err }) => { const statusCode = res ? res.statusCode : err ? err.statusCode : 404; return { statusCode }; };
getServerSideProps 错误处理
javascriptexport async function getServerSideProps(context) { try { const data = await fetchData(); if (!data) { return { notFound: true, // 返回 404 页面 }; } return { props: { data }, }; } catch (error) { return { props: { error: error.message }, }; } }
服务器组件错误处理
使用 try-catch
javascriptasync function ProductPage({ params }) { try { const product = await fetchProduct(params.id); if (!product) { notFound(); // 返回 404 页面 } return <ProductDetails product={product} />; } catch (error) { console.error('Failed to fetch product:', error); throw error; // 触发最近的错误边界 } }
使用 notFound()
javascriptimport { notFound } from 'next/navigation'; async function BlogPost({ params }) { const post = await getPostBySlug(params.slug); if (!post) { notFound(); // 返回 404 页面 } return <PostContent post={post} />; }
自定义 not-found 页面
javascript// app/not-found.js export default function NotFound() { return ( <div> <h2>Page Not Found</h2> <p>The page you're looking for doesn't exist.</p> <Link href="/">Go Home</Link> </div> ); } // app/blog/not-found.js export default function BlogNotFound() { return ( <div> <h2>Blog Post Not Found</h2> <p>The blog post you're looking for doesn't exist.</p> <Link href="/blog">Back to Blog</Link> </div> ); }
API Routes 错误处理
错误响应格式
javascript// app/api/hello/route.js export async function GET(request) { try { const data = await fetchData(); return Response.json(data); } catch (error) { console.error('API Error:', error); return Response.json( { error: 'Internal Server Error', message: error.message, timestamp: new Date().toISOString(), }, { status: 500 } ); } }
验证错误
javascriptimport { z } from 'zod'; const schema = z.object({ email: z.string().email(), password: z.string().min(8), }); export async function POST(request) { try { const body = await request.json(); // 验证输入 const validated = schema.parse(body); // 处理请求 const result = await createUser(validated); return Response.json(result, { status: 201 }); } catch (error) { if (error instanceof z.ZodError) { return Response.json( { error: 'Validation Error', details: error.errors, }, { status: 400 } ); } return Response.json( { error: 'Internal Server Error' }, { status: 500 } ); } }
认证错误
javascriptexport async function GET(request) { const token = request.headers.get('authorization'); if (!token) { return Response.json( { error: 'Unauthorized' }, { status: 401 } ); } try { const user = await verifyToken(token); return Response.json({ user }); } catch (error) { return Response.json( { error: 'Invalid token' }, { status: 401 } ); } }
客户端错误处理
Error Boundary 组件
javascript'use client'; import { Component, ReactNode } from 'react'; interface Props { children: ReactNode; fallback?: ReactNode; } interface State { hasError: boolean; error?: Error; } export class ErrorBoundary extends Component<Props, State> { constructor(props: Props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: any) { console.error('Error caught by boundary:', error, errorInfo); // 发送错误到日志服务 logErrorToService(error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback || ( <div> <h2>Something went wrong</h2> <p>{this.state.error?.message}</p> <button onClick={() => this.setState({ hasError: false })}> Try again </button> </div> ); } return this.props.children; } }
使用 Error Boundary
javascript'use client'; import { ErrorBoundary } from '@/components/ErrorBoundary'; export default function Page() { return ( <ErrorBoundary fallback={ <div> <h2>Component Error</h2> <button onClick={() => window.location.reload()}> Reload Page </button> </div> } > <RiskyComponent /> </ErrorBoundary> ); }
异步错误处理
javascript'use client'; import { useEffect } from 'react'; export default function AsyncComponent() { useEffect(() => { const fetchData = async () => { try { const data = await fetch('/api/data').then(r => r.json()); console.log(data); } catch (error) { console.error('Fetch error:', error); // 显示错误消息 showErrorToast('Failed to fetch data'); } }; fetchData(); }, []); return <div>Loading...</div>; }
全局错误处理
使用 window.onerror
javascript// app/layout.js 'use client'; import { useEffect } from 'react'; export default function RootLayout({ children }) { useEffect(() => { const handleError = (event: ErrorEvent) => { console.error('Global error:', event.error); // 发送到错误跟踪服务 logErrorToService({ message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error, }); }; window.addEventListener('error', handleError); return () => { window.removeEventListener('error', handleError); }; }, []); return ( <html lang="en"> <body>{children}</body> </html> ); }
使用 window.onunhandledrejection
javascriptuseEffect(() => { const handleUnhandledRejection = (event: PromiseRejectionEvent) => { console.error('Unhandled promise rejection:', event.reason); // 发送到错误跟踪服务 logErrorToService({ type: 'unhandledRejection', reason: event.reason, }); // 防止默认的控制台错误 event.preventDefault(); }; window.addEventListener('unhandledrejection', handleUnhandledRejection); return () => { window.removeEventListener('unhandledrejection', handleUnhandledRejection); }; }, []);
错误日志和监控
使用 Sentry
javascript// sentry.client.config.js import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, }); // sentry.server.config.js import * as Sentry from '@sentry/nextjs'; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, environment: process.env.NODE_ENV, tracesSampleRate: 1.0, });
自定义错误日志
javascript// lib/errorLogger.js export function logError(error: Error, context?: any) { const errorData = { message: error.message, stack: error.stack, timestamp: new Date().toISOString(), context, }; // 发送到日志服务 if (typeof window !== 'undefined') { // 客户端 fetch('/api/log-error', { method: 'POST', body: JSON.stringify(errorData), }); } else { // 服务器端 console.error('Server error:', errorData); } } export function logApiError(error: any, request: Request) { const errorData = { url: request.url, method: request.method, error: error.message, stack: error.stack, timestamp: new Date().toISOString(), }; console.error('API error:', errorData); }
最佳实践
1. 提供有用的错误信息
javascript// ✅ 好的做法 export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong</h2> <p>We couldn't load the page. Please try again.</p> <button onClick={reset}>Try again</button> <Link href="/">Go Home</Link> </div> ); } // ❌ 不好的做法 export default function Error({ error }) { return ( <div> <p>{error.message}</p> </div> ); }
2. 记录错误上下文
javascript// ✅ 好的做法 try { const user = await getUser(userId); const posts = await getPosts(user.id); return { user, posts }; } catch (error) { logError(error, { userId, action: 'fetchUserPosts' }); throw error; } // ❌ 不好的做法 try { const user = await getUser(userId); const posts = await getPosts(user.id); return { user, posts }; } catch (error) { console.error(error); throw error; }
3. 提供恢复选项
javascript// ✅ 好的做法 export default function Error({ error, reset }) { return ( <div> <h2>Something went wrong</h2> <button onClick={reset}>Try again</button> <button onClick={() => window.location.href = '/'}> Go Home </button> </div> ); } // ❌ 不好的做法 export default function Error({ error }) { return ( <div> <h2>Something went wrong</h2> <p>Please refresh the page</p> </div> ); }
4. 区分开发和生产环境
javascriptexport default function Error({ error, reset }) { const isDev = process.env.NODE_ENV === 'development'; return ( <div> <h2>Something went wrong</h2> {isDev ? ( <details> <summary>Error Details</summary> <pre>{error.message}</pre> <pre>{error.stack}</pre> </details> ) : ( <p>We're sorry for the inconvenience. Please try again.</p> )} <button onClick={reset}>Try again</button> </div> ); }
5. 使用类型安全的错误处理
javascript// types/errors.ts export class AppError extends Error { constructor( message: string, public statusCode: number = 500, public code?: string ) { super(message); this.name = 'AppError'; } } export class NotFoundError extends AppError { constructor(message: string = 'Resource not found') { super(message, 404, 'NOT_FOUND'); } } export class ValidationError extends AppError { constructor(message: string = 'Validation failed') { super(message, 400, 'VALIDATION_ERROR'); } } // 使用 export async function getProduct(id: string) { const product = await db.product.findUnique({ where: { id } }); if (!product) { throw new NotFoundError(`Product ${id} not found`); } return product; }
通过合理使用这些错误处理机制,可以构建出健壮、可靠的 Next.js 应用,提供良好的用户体验。