Next.js错误处理 #

一、错误处理概述 #

1.1 错误类型 #

错误类型 处理文件 说明
运行时错误 error.tsx 组件渲染错误
全局错误 global-error.tsx 根布局错误
404错误 not-found.tsx 页面未找到
服务端错误 try/catch API错误

1.2 错误边界 #

Next.js 使用 React 错误边界捕获错误,错误边界必须是一个客户端组件。

二、error.tsx #

2.1 基本用法 #

tsx
'use client'

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    return (
        <div>
            <h2>出错了!</h2>
            <button onClick={() => reset()}>重试</button>
        </div>
    )
}

2.2 错误属性 #

属性 类型 说明
error Error 错误对象
error.digest string 错误摘要(服务端错误)
reset () => void 重试函数

2.3 完整错误页面 #

tsx
'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 className="min-h-screen flex items-center justify-center bg-gray-50">
            <div className="max-w-md w-full text-center">
                <div className="text-6xl mb-4">😢</div>
                <h2 className="text-2xl font-bold mb-4">出错了!</h2>
                <p className="text-gray-600 mb-6">
                    {error.message || '发生了意外错误'}
                </p>
                <div className="flex gap-4 justify-center">
                    <button
                        onClick={() => reset()}
                        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
                    >
                        重试
                    </button>
                    <a
                        href="/"
                        className="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
                    >
                        返回首页
                    </a>
                </div>
            </div>
        </div>
    )
}

2.4 错误日志 #

tsx
'use client'

import { useEffect } from 'react'

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    useEffect(() => {
        fetch('/api/log-error', {
            method: 'POST',
            body: JSON.stringify({
                message: error.message,
                stack: error.stack,
                digest: error.digest,
                url: window.location.href,
                timestamp: new Date().toISOString(),
            }),
        })
    }, [error])
    
    return (
        <div>
            <h2>出错了</h2>
            <button onClick={reset}>重试</button>
        </div>
    )
}

三、global-error.tsx #

3.1 概念 #

global-error.tsx 用于处理根布局中的错误,必须包含 html 和 body 标签。

3.2 基本用法 #

tsx
'use client'

export default function GlobalError({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    return (
        <html lang="zh-CN">
            <body>
                <div className="min-h-screen flex items-center justify-center">
                    <div className="text-center">
                        <h2>严重错误</h2>
                        <button onClick={() => reset()}>重试</button>
                    </div>
                </div>
            </body>
        </html>
    )
}

3.3 完整示例 #

tsx
'use client'

import { useEffect } from 'react'

export default function GlobalError({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    useEffect(() => {
        console.error('全局错误:', error)
    }, [error])
    
    return (
        <html lang="zh-CN">
            <head>
                <meta charSet="utf-8" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <title>错误</title>
            </head>
            <body className="bg-gray-100">
                <div className="min-h-screen flex items-center justify-center">
                    <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
                        <div className="text-6xl mb-4">⚠️</div>
                        <h1 className="text-2xl font-bold text-red-600 mb-4">
                            系统错误
                        </h1>
                        <p className="text-gray-600 mb-6">
                            应用遇到了严重错误,请刷新页面重试
                        </p>
                        <button
                            onClick={() => reset()}
                            className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
                        >
                            刷新页面
                        </button>
                    </div>
                </div>
            </body>
        </html>
    )
}

四、not-found.tsx #

4.1 基本用法 #

tsx
import Link from 'next/link'

export default function NotFound() {
    return (
        <div className="min-h-screen flex items-center justify-center">
            <div className="text-center">
                <h1 className="text-6xl font-bold text-gray-300">404</h1>
                <h2 className="text-2xl font-medium mt-4">页面未找到</h2>
                <p className="text-gray-500 mt-2">
                    您访问的页面不存在
                </p>
                <Link
                    href="/"
                    className="mt-6 inline-block px-4 py-2 bg-blue-500 text-white rounded"
                >
                    返回首页
                </Link>
            </div>
        </div>
    )
}

4.2 触发404 #

使用notFound函数

tsx
import { notFound } from 'next/navigation'

interface PageProps {
    params: Promise<{ id: string }>
}

export default async function ProductPage({ params }: PageProps) {
    const { id } = await params
    const product = await getProduct(id)
    
    if (!product) {
        notFound()
    }
    
    return <div>{product.name}</div>
}

4.3 嵌套404页面 #

text
app/
├── not-found.tsx         # 全局404
├── blog/
│   ├── not-found.tsx     # 博客404
│   └── [slug]/
│       └── page.tsx

博客404页面

tsx
import Link from 'next/link'

export default function BlogNotFound() {
    return (
        <div className="text-center py-16">
            <h2 className="text-2xl font-bold mb-4">文章未找到</h2>
            <p className="text-gray-500 mb-6">
                您访问的文章不存在或已被删除
            </p>
            <Link href="/blog" className="text-blue-500 hover:underline">
                查看所有文章
            </Link>
        </div>
    )
}

五、加载状态 #

5.1 loading.tsx #

tsx
export default function Loading() {
    return (
        <div className="min-h-screen flex items-center justify-center">
            <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
        </div>
    )
}

5.2 骨架屏 #

tsx
export default function Loading() {
    return (
        <div className="animate-pulse">
            <div className="h-8 bg-gray-200 rounded w-1/2 mb-4"></div>
            <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
            <div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
            <div className="h-64 bg-gray-200 rounded mb-4"></div>
            <div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
            <div className="h-4 bg-gray-200 rounded w-5/6"></div>
        </div>
    )
}

5.3 目录结构 #

text
app/
├── loading.tsx           # 全局加载
├── blog/
│   ├── loading.tsx       # 博客加载
│   └── page.tsx

六、服务端错误处理 #

6.1 try/catch #

tsx
interface PageProps {
    params: Promise<{ id: string }>
}

export default async function Page({ params }: PageProps) {
    try {
        const { id } = await params
        const data = await getData(id)
        
        return <div>{data.title}</div>
    } catch (error) {
        console.error('获取数据失败:', error)
        throw new Error('获取数据失败')
    }
}

6.2 错误边界嵌套 #

tsx
import { ErrorBoundary } from 'react-error-boundary'

function ErrorFallback({ error, resetErrorBoundary }) {
    return (
        <div role="alert">
            <p>出错了:</p>
            <pre>{error.message}</pre>
            <button onClick={resetErrorBoundary}>重试</button>
        </div>
    )
}

export default function Page() {
    return (
        <ErrorBoundary FallbackComponent={ErrorFallback}>
            <MyComponent />
        </ErrorBoundary>
    )
}

七、错误处理最佳实践 #

7.1 错误分类 #

tsx
'use client'

import { useEffect } from 'react'

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    useEffect(() => {
        if (error.digest) {
            console.error('服务端错误:', error.digest)
        } else {
            console.error('客户端错误:', error.message)
        }
    }, [error])
    
    return (
        <div>
            <h2>出错了</h2>
            <button onClick={reset}>重试</button>
        </div>
    )
}

7.2 错误恢复 #

tsx
'use client'

import { useState } from 'react'

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    const [retryCount, setRetryCount] = useState(0)
    
    const handleRetry = () => {
        setRetryCount(prev => prev + 1)
        reset()
    }
    
    return (
        <div>
            <h2>出错了</h2>
            <p>重试次数: {retryCount}</p>
            <button onClick={handleRetry}>重试</button>
        </div>
    )
}

7.3 错误上报 #

tsx
'use client'

import { useEffect } from 'react'

function reportError(error: Error, context: Record<string, any>) {
    if (typeof window !== 'undefined' && window.Sentry) {
        window.Sentry.captureException(error, { extra: context })
    }
}

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    useEffect(() => {
        reportError(error, {
            url: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: new Date().toISOString(),
        })
    }, [error])
    
    return (
        <div>
            <h2>出错了</h2>
            <button onClick={reset}>重试</button>
        </div>
    )
}

八、综合示例 #

8.1 完整错误处理 #

text
app/
├── error.tsx             # 全局错误
├── global-error.tsx      # 根布局错误
├── not-found.tsx         # 404页面
├── loading.tsx           # 加载状态
└── dashboard/
    ├── error.tsx         # 仪表盘错误
    ├── not-found.tsx     # 仪表盘404
    └── loading.tsx       # 仪表盘加载

全局错误

tsx
'use client'

import { useEffect } from 'react'
import Link from 'next/link'

export default function Error({
    error,
    reset,
}: {
    error: Error & { digest?: string }
    reset: () => void
}) {
    useEffect(() => {
        console.error('错误:', error)
    }, [error])
    
    return (
        <div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
            <div className="max-w-lg w-full bg-white rounded-lg shadow-lg p-8">
                <div className="text-center">
                    <div className="text-5xl mb-4">😵</div>
                    <h1 className="text-2xl font-bold text-gray-900 mb-2">
                        出错了
                    </h1>
                    <p className="text-gray-600 mb-6">
                        {error.message || '发生了意外错误,请稍后重试'}
                    </p>
                </div>
                <div className="flex flex-col sm:flex-row gap-3 justify-center">
                    <button
                        onClick={reset}
                        className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition"
                    >
                        重试
                    </button>
                    <Link
                        href="/"
                        className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition text-center"
                    >
                        返回首页
                    </Link>
                </div>
                {process.env.NODE_ENV === 'development' && (
                    <details className="mt-6">
                        <summary className="text-sm text-gray-500 cursor-pointer">
                            错误详情
                        </summary>
                        <pre className="mt-2 p-4 bg-gray-100 rounded text-xs overflow-auto">
                            {error.stack}
                        </pre>
                    </details>
                )}
            </div>
        </div>
    )
}

九、总结 #

错误处理要点:

文件 用途 特点
error.tsx 运行时错误 客户端组件
global-error.tsx 根布局错误 包含html/body
not-found.tsx 404页面 可嵌套
loading.tsx 加载状态 自动显示

下一步,让我们学习数据获取!

最后更新:2026-03-28