错误处理
深入理解 Next.js 错误处理机制,包括预期错误、未捕获异常、错误边界、404 处理和优雅降级策略
概述
错误可以分为两类:
- 预期错误:在应用程序正常运行期间可能发生的错误,例如服务器端表单验证或失败的请求
- 未捕获异常:意外的运行时错误
Next.js 提供了不同的机制来处理这两类错误。
处理预期错误
预期错误应该被显式处理并返回给客户端。对于这些错误,避免使用 try/catch 块和抛出错误,而是将预期错误建模为返回值。
Server Functions 中的预期错误
使用 useActionState hook 处理 Server Functions 中的预期错误:
'use server'
export async function createPost(prevState: any, formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
const res = await fetch('https://api.vercel.app/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
})
if (!res.ok) {
return { message: 'Failed to create post' }
}
return { message: 'Post created successfully' }
}在客户端组件中使用:
'use client'
import { useActionState } from 'react'
import { createPost } from './actions'
const initialState = {
message: '',
}
export default function Page() {
const [state, formAction, isPending] = useActionState(
createPost,
initialState
)
return (
<form action={formAction}>
<input type="text" name="title" />
<textarea name="content" />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
{state.message && <p>{state.message}</p>}
</form>
)
}Server Components 中的预期错误
在 Server Components 中,可以使用 redirect 将用户重定向到错误页面:
import { redirect } from 'next/navigation'
export default async function Page() {
const res = await fetch('https://...')
if (!res.ok) {
redirect('/error')
}
const data = await res.json()
return <div>{data.name}</div>
}处理未捕获异常
未捕获异常是意外错误,表示应用程序正常流程中的错误或问题。这些应该通过抛出错误来处理,然后由错误边界捕获。
嵌套错误边界
通过在路由段内添加 error.js 文件并导出 React 组件来创建错误边界:
'use client' // 错误边界必须是客户端组件
import { useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
// 将错误记录到错误报告服务
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}工作原理
error.js将路由段及其嵌套子级包装在 React 错误边界中- 当边界内抛出错误时,错误组件显示为后备 UI
- 错误边界内的错误会向上冒泡到最近的父错误边界
- 这允许通过在路由层次结构的不同级别放置
error.js文件来进行细粒度的错误处理
Props
error
转发到 error.js 客户端组件的 Error 对象实例。
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
return (
<div>
<h2>Error: {error.message}</h2>
<p>Digest: {error.digest}</p>
</div>
)
}在生产环境中,转发到客户端的 Error 对象不包含原始错误的详细信息,以避免泄露敏感信息。error.message 包含有关错误的通用消息,error.digest 包含可用于匹配服务器端日志中相应错误的自动生成的错误哈希。
在开发期间,转发到客户端的 Error 对象将被序列化并包含原始错误的 message 以便于调试。
reset
重置错误边界的函数。执行时,函数将尝试重新渲染错误边界的内容。如果成功,后备错误组件将被重新渲染的结果替换。
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
)
}在 Server Components 中抛出错误
在 Server Components 中抛出的错误将由最近的父错误边界处理:
export default async function Page() {
const res = await fetch('https://...')
if (!res.ok) {
throw new Error('Failed to fetch data')
}
const data = await res.json()
return <div>{data.name}</div>
}在 Client Components 中抛出错误
在 Client Components 中,在事件处理器内抛出错误时使用 startTransition:
'use client'
import { useTransition } from 'react'
export default function Page() {
const [isPending, startTransition] = useTransition()
const handleClick = () =>
startTransition(() => {
throw new Error('Exception')
})
return (
<button type="button" onClick={handleClick}>
Click me
</button>
)
}全局错误
虽然不太常见,但可以使用位于根 app 目录中的 global-error.js 文件处理根布局中的错误,即使在使用国际化时也是如此。
全局错误 UI 必须定义自己的 <html> 和 <body> 标签,因为它在活动时替换根布局或模板。
'use client' // 错误边界必须是客户端组件
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
// global-error 必须包含 html 和 body 标签
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}404 处理
Next.js 提供了两种约定来处理 404 情况:
not-found.js:当在路由段中调用notFound函数时使用global-not-found.js:用于定义整个应用程序中未匹配路由的全局 404 页面
notFound 函数
notFound 函数允许你在路由段内渲染 not-found 文件,并注入 <meta name="robots" content="noindex" /> 标签。
import { notFound } from 'next/navigation'
async function fetchUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`)
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const user = await fetchUser(id)
if (!user) {
notFound()
}
return <div>User: {user.name}</div>
}not-found.js 文件
not-found.js 文件用于在路由段内调用 notFound 函数时渲染 UI。
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}HTTP 状态码
Next.js 将为流式响应返回 200 HTTP 状态码,为非流式响应返回 404。
数据获取
可以在 not-found.js 中获取数据以显示更多上下文:
import Link from 'next/link'
import { headers } from 'next/headers'
export default async function NotFound() {
const headersList = await headers()
const domain = headersList.get('host')
const data = await getSiteData(domain)
return (
<div>
<h2>Not Found: {data.name}</h2>
<p>Could not find requested resource</p>
<p>
View <Link href="/blog">all posts</Link>
</p>
</div>
)
}如果需要使用客户端组件 hooks(如 usePathname)来根据路径显示内容,必须在客户端获取数据。
global-not-found.js(实验性)
global-not-found.js 文件允许你为整个应用程序定义 404 页面。与在路由级别工作的 not-found.js 不同,当请求的 URL 根本不匹配任何路由时使用此文件。
Next.js 跳过渲染并直接返回此全局页面。
import Link from 'next/link'
export default function GlobalNotFound() {
return (
<html lang="en">
<body>
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link href="/">Go back home</Link>
</div>
</body>
</html>
)
}启用 global-not-found.js
在 next.config.js 中启用实验性功能:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
globalNotFound: true,
},
}
export default nextConfig元数据
可以导出 metadata 对象或 generateMetadata 函数来自定义 404 页面的元数据:
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Not Found',
description: 'The page you are looking for does not exist.',
}
export default function GlobalNotFound() {
return (
<html lang="en">
<body>
<div>
<h1>Not Found</h1>
<p>The page you are looking for does not exist.</p>
</div>
</body>
</html>
)
}Next.js 自动为返回 404 状态码的页面(包括 global-not-found.js 页面)注入 <meta name="robots" content="noindex" />。
优雅的错误恢复
自定义错误边界
当客户端渲染失败时,显示最后已知的服务器渲染 UI 可以提供更好的用户体验:
'use client'
import React, { Component, createRef } from 'react'
class GracefullyDegradingErrorBoundary extends Component<
{ children: React.ReactNode; onError?: (error: Error, errorInfo: React.ErrorInfo) => void },
{ hasError: boolean }
> {
contentRef = createRef<HTMLDivElement>()
constructor(props: any) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(_: Error) {
return { hasError: true }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
if (this.props.onError) {
this.props.onError(error, errorInfo)
}
}
render() {
if (this.state.hasError) {
// 渲染当前 HTML 内容而不进行水合
return (
<>
<div
ref={this.contentRef}
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: this.contentRef.current?.innerHTML || '',
}}
/>
<div className="fixed bottom-0 left-0 right-0 bg-red-600 text-white py-4 px-6 text-center">
<p className="font-semibold">
An error occurred during page rendering
</p>
</div>
</>
)
}
return <div ref={this.contentRef}>{this.props.children}</div>
}
}
export default GracefullyDegradingErrorBoundary错误处理最佳实践
1. 区分预期错误和未捕获异常
- 预期错误:使用返回值,不要抛出错误
- 未捕获异常:使用错误边界捕获
// 预期错误 - 返回错误状态
export async function createPost(formData: FormData) {
'use server'
if (!formData.get('title')) {
return { error: 'Title is required' }
}
// 处理逻辑
}
// 未捕获异常 - 抛出错误
export default async function Page() {
const data = await fetch('https://...')
if (!data.ok) {
throw new Error('Failed to fetch')
}
return <div>{/* 内容 */}</div>
}2. 使用嵌套错误边界进行细粒度控制
在不同的路由层级放置 error.js 文件以提供更具体的错误处理:
app/
├── error.tsx # 根级别错误边界
├── dashboard/
│ ├── error.tsx # 仪表板错误边界
│ └── settings/
│ └── error.tsx # 设置错误边界3. 记录错误到监控服务
在错误组件中使用 useEffect 记录错误:
'use client'
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
useEffect(() => {
// 记录到 Sentry 或其他服务
Sentry.captureException(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<p>Error ID: {error.digest}</p>
</div>
)
}4. 提供有意义的错误消息
根据环境提供不同级别的错误详情:
'use client'
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
const isDevelopment = process.env.NODE_ENV === 'development'
return (
<div>
<h2>Something went wrong!</h2>
{isDevelopment && (
<>
<p>Error: {error.message}</p>
<pre>{error.stack}</pre>
</>
)}
{!isDevelopment && (
<p>We're working on fixing this issue. Please try again later.</p>
)}
</div>
)
}5. 实现重试逻辑
使用 reset 函数允许用户重试操作:
'use client'
import { useState } from 'react'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
const [retryCount, setRetryCount] = useState(0)
const handleReset = () => {
setRetryCount(prev => prev + 1)
reset()
}
return (
<div>
<h2>Something went wrong!</h2>
<p>Retry attempts: {retryCount}</p>
<button onClick={handleReset}>
Try again
</button>
</div>
)
}6. 使用 notFound 处理资源不存在
对于资源不存在的情况,使用 notFound 而不是抛出错误:
import { notFound } from 'next/navigation'
export default async function Post({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
if (!post) {
notFound() // 使用 notFound 而不是 throw new Error
}
return <article>{post.content}</article>
}常见错误处理模式
表单验证错误
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function signup(prevState: any, formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// 处理注册逻辑
return { success: true }
}'use client'
import { useActionState } from 'react'
import { signup } from './actions'
export default function SignupPage() {
const [state, formAction, isPending] = useActionState(signup, null)
return (
<form action={formAction}>
<input type="email" name="email" />
{state?.errors?.email && <p>{state.errors.email}</p>}
<input type="password" name="password" />
{state?.errors?.password && <p>{state.errors.password}</p>}
<button type="submit" disabled={isPending}>
Sign up
</button>
</form>
)
}API 错误处理
export default async function Dashboard() {
try {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store',
})
if (!res.ok) {
// 对于 API 错误,抛出以触发错误边界
throw new Error(`API Error: ${res.status}`)
}
const data = await res.json()
return <div>{data.content}</div>
} catch (error) {
// 这将被最近的错误边界捕获
throw error
}
}条件错误边界
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
// 根据错误类型显示不同的 UI
if (error.message.includes('API Error: 401')) {
return (
<div>
<h2>Unauthorized</h2>
<p>Please log in to continue.</p>
<a href="/login">Go to login</a>
</div>
)
}
if (error.message.includes('API Error: 404')) {
return (
<div>
<h2>Not Found</h2>
<p>The resource you're looking for doesn't exist.</p>
<button onClick={reset}>Go back</button>
</div>
)
}
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
)
}总结
Next.js 提供了全面的错误处理机制:
- 预期错误:使用返回值和
useActionState处理表单验证和已知错误 - 未捕获异常:使用错误边界(
error.js、global-error.js)捕获运行时错误 - 404 处理:使用
notFound函数和not-found.js/global-not-found.js文件 - 嵌套边界:在不同路由层级提供细粒度的错误处理
- 优雅降级:使用自定义错误边界保留服务器渲染的 UI
- 错误恢复:使用
reset函数允许用户重试操作
通过合理使用这些错误处理机制,可以构建既健壮又用户友好的应用程序。
上次更新于