6月5日 21:52

Next.js 错误处理全指南:error.tsx、global-error 和 API 错误分层方案

Next.js 应用出错时,用户看到的不能是一个白屏或一堆报错代码。App Router 提供了分层错误处理机制——从组件级到全局级,每一层都有专门的文件处理。搞清这些层级,就能让错误发生时用户仍然能看到有意义的提示,而不是整个应用崩溃。

错误处理层级

shell
组件内 try/catch → 处理可预期的业务错误 ↓ 未捕获 error.tsx → 路由级 Error Boundary ↓ 仍未捕获 global-error.tsx → 全局兜底(根 layout 也崩了)

从内到外,每一层兜住上一层没处理的错误。

error.tsx:路由级错误边界

App Router 中,每个路由段可以有一个 error.tsx,捕获该路由段及其子组件的运行时错误:

typescript
// app/dashboard/error.tsx 'use client' // 必须是 Client Component export default function DashboardError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div className="error-container"> <h2>出错了</h2> <p>{error.message}</p> <button onClick={reset}>重试</button> </div> ) }

关键点

  • error.tsx 必须是 Client Component('use client')——Error Boundary 是客户端概念
  • reset 函数重新执行渲染该路由段的尝试——适合临时性错误(网络波动)
  • error.digest 是 Next.js 生成的错误哈希,可以在日志中追踪

error.tsx 捕获什么

  • 子组件的运行时错误
  • 子组件中 Server Component 的数据获取失败
  • 子路由的 page.tsx 抛出的错误

error.tsx 不捕获什么

  • 同级 layout.tsx 的错误——layout 在 error boundary 外面
  • loading.tsx 不会影响 error.tsx
  • app/layout.tsx 的错误——需要 global-error.tsx

global-error.tsx:最后的兜底

当根 layout 崩溃时,error.tsx 也渲染不了(因为 layout 本身都挂了)。这时需要 global-error.tsx

typescript
// app/global-error.tsx 'use client' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <html> <body> <h2>应用发生了严重错误</h2> <button onClick={reset}>重新加载</button> </body> </html> ) }

global-error.tsx 必须自带 <html><body>——因为根 layout 已经崩溃,什么都复用不了。这个文件应该尽量简单,不要引入任何可能也出错的组件。

Server Component 中的错误处理

Server Component 里用 try/catch 处理可预期的错误:

typescript
// app/dashboard/page.tsx export default async function DashboardPage() { let stats try { stats = await fetchDashboardStats() } catch { stats = { activeUsers: 0, revenue: 0 } // 降级数据 } return ( <div> <h1>仪表盘</h1> <StatsCard data={stats} /> </div> ) }

可预期的错误(API 超时、数据不存在)用 try/catch 降级处理,不让它冒泡到 error.tsx。只有真正无法恢复的错误才让 Error Boundary 接管。

API Route 的错误处理

基本错误响应

typescript
// app/api/users/route.ts import { NextResponse } from 'next/server' export async function GET(request: Request) { try { const users = await fetchUsers() return NextResponse.json(users) } catch (error) { console.error('Failed to fetch users:', error) return NextResponse.json( { error: '获取用户列表失败' }, { status: 500 } ) } }

业务异常封装

typescript
// lib/errors.ts export class AppError extends Error { constructor( public statusCode: number, public code: string, message: string, ) { super(message) } } // app/api/users/route.ts export async function POST(request: Request) { const body = await request.json() if (!body.email) { throw new AppError(400, 'MISSING_EMAIL', '邮箱不能为空') } if (await isEmailTaken(body.email)) { throw new AppError(409, 'EMAIL_TAKEN', '邮箱已被注册') } const user = await createUser(body) return NextResponse.json(user, { status: 201 }) }

全局错误中间件

在 middleware 或单独的 error handler 中统一处理 AppError:

typescript
// 在 route handler 外层包一个 wrapper function withErrorHandler(handler: Function) { return async (...args: any[]) => { try { return await handler(...args) } catch (error) { if (error instanceof AppError) { return NextResponse.json( { error: error.code, message: error.message }, { status: error.statusCode } ) } console.error('Unexpected error:', error) return NextResponse.json( { error: 'INTERNAL_ERROR', message: '服务器内部错误' }, { status: 500 } ) } } } // 使用 export const POST = withErrorHandler(async (request: Request) => { const body = await request.json() // ... 业务逻辑,直接 throw AppError })

notFound:比 Error Boundary 更优雅的"错误"

"找不到"不是错误,是一种正常状态。用 notFound() 比抛异常更语义化:

typescript
// app/products/[id]/page.tsx import { notFound } from 'next/navigation' export default async function ProductPage({ params }: { params: { id: string } }) { const product = await getProduct(params.id) if (!product) { notFound() // 触发 not-found.tsx,不触发 error.tsx } return <ProductDetail product={product} /> }

自定义 not-found 页面:

typescript
// app/not-found.tsx export default function NotFound() { return ( <div className="not-found"> <h2>页面不存在</h2> <a href="/">返回首页</a> </div> ) }

notFound() 的好处:它不会在日志中记录为错误,不会触发 Error Boundary,返回 404 状态码——语义更准确。

Client Component 中的错误处理

Client Component 里可以用 React 的 Error Boundary class 组件做更细粒度的捕获:

typescript
// components/error-boundary.tsx 'use client' import { Component } from 'react' type Props = { fallback: React.ReactNode children: React.ReactNode } export class ErrorBoundary extends Component<Props, { hasError: boolean }> { state = { hasError: false } static getDerivedStateFromError() { return { hasError: true } } render() { if (this.state.hasError) { return this.props.fallback } return this.props.children } } // 使用 <ErrorBoundary fallback={<p>加载失败</p>}> <Chart data={data} /> </ErrorBoundary>

这比 error.tsx 更灵活——可以包在单个组件外面,不影响整个页面。

错误监控和日志

生产环境中,错误信息不能只在前端打印——需要上报到监控系统:

typescript
// app/error.tsx 'use client' import { useEffect } from 'react' export default function Error({ error, reset }: { error: Error; reset: () => void }) { useEffect(() => { // 上报错误到监控系统 reportError(error) }, [error]) return ( <div> <h2>出了点问题</h2> <button onClick={reset}>重试</button> </div> ) }

Server Component 中的错误上报:

typescript
// app/dashboard/page.tsx export default async function DashboardPage() { try { const data = await fetchData() return <Dashboard data={data} /> } catch (error) { reportError(error) // 上报 throw error // 让 error.tsx 处理 UI } }

错误处理策略速查

错误类型处理方式文件位置
数据获取失败try/catch 降级Server Component 内
路由级运行时错误error.tsxapp/[segment]/error.tsx
根 layout 崩溃global-error.tsxapp/global-error.tsx
页面不存在notFound() + not-found.tsxapp/not-found.tsx
API 错误try/catch + 状态码route.ts 内
单组件错误ErrorBoundary 包裹任意 Client Component
未预期错误上报监控 + 兜底 UIerror.tsx + 监控
标签:Next.js