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.tsx | app/[segment]/error.tsx |
| 根 layout 崩溃 | global-error.tsx | app/global-error.tsx |
| 页面不存在 | notFound() + not-found.tsx | app/not-found.tsx |
| API 错误 | try/catch + 状态码 | route.ts 内 |
| 单组件错误 | ErrorBoundary 包裹 | 任意 Client Component |
| 未预期错误 | 上报监控 + 兜底 UI | error.tsx + 监控 |