Skip to content

Next.js 错误处理 🚨

🎯 概述

Next.js 提供了完善的错误处理机制,包括错误边界、自定义错误页面、全局错误处理等功能,帮助开发者优雅地处理应用中的各种错误情况。

📦 错误类型

1. 客户端错误

  • 组件渲染错误
  • 事件处理错误
  • 异步操作错误

2. 服务端错误

  • 数据获取错误
  • API 路由错误
  • 服务器渲染错误

3. 路由错误

  • 404 页面未找到
  • 500 服务器错误

🚀 error.tsx 文件

基本用法

tsx
// app/error.tsx
'use client'

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

嵌套错误处理

tsx
// app/dashboard/error.tsx
'use client'

export default function DashboardError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div className="error-container">
      <h2>仪表板加载失败</h2>
      <p>{error.message}</p>
      <button onClick={reset}>重新加载</button>
    </div>
  )
}

📝 全局错误处理

global-error.tsx

tsx
// app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <h2>应用出现严重错误</h2>
        <p>{error.message}</p>
        <button onClick={() => reset()}>重启应用</button>
      </body>
    </html>
  )
}

🎨 自定义 404 页面

not-found.tsx

tsx
// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="not-found">
      <h1>404</h1>
      <h2>页面未找到</h2>
      <p>抱歉,您访问的页面不存在。</p>
      <Link href="/">返回首页</Link>
    </div>
  )
}

触发 404

tsx
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`)
  if (!res.ok) return null
  return res.json()
}

export default async function Post({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
  
  if (!post) {
    notFound()
  }
  
  return <article>{post.title}</article>
}

🔄 错误边界

React Error Boundary

tsx
// components/ErrorBoundary.tsx
'use client'

import React from 'react'

interface Props {
  children: React.ReactNode
  fallback?: React.ReactNode
}

interface State {
  hasError: boolean
  error?: Error
}

export class ErrorBoundary extends React.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: React.ErrorInfo) {
    console.error('Error caught by boundary:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <div>出错了</div>
    }

    return this.props.children
  }
}

使用:

tsx
<ErrorBoundary fallback={<div>加载失败</div>}>
  <MyComponent />
</ErrorBoundary>

📊 API 路由错误处理

基本错误处理

tsx
// app/api/users/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  try {
    const users = await fetchUsers()
    return NextResponse.json(users)
  } catch (error) {
    console.error('获取用户失败:', error)
    return NextResponse.json(
      { error: '获取用户失败' },
      { status: 500 }
    )
  }
}

自定义错误类

tsx
// lib/errors.ts
export class APIError extends Error {
  constructor(
    message: string,
    public statusCode: number = 500,
    public code?: string
  ) {
    super(message)
    this.name = 'APIError'
  }
}

// app/api/posts/route.ts
import { APIError } from '@/lib/errors'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  try {
    const body = await request.json()
    
    if (!body.title) {
      throw new APIError('标题不能为空', 400, 'MISSING_TITLE')
    }
    
    const post = await createPost(body)
    return NextResponse.json(post)
    
  } catch (error) {
    if (error instanceof APIError) {
      return NextResponse.json(
        { error: error.message, code: error.code },
        { status: error.statusCode }
      )
    }
    
    return NextResponse.json(
      { error: '服务器错误' },
      { status: 500 }
    )
  }
}

🎯 数据获取错误处理

Server Component 错误处理

tsx
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }
  })
  
  if (!res.ok) {
    throw new Error('获取文章失败')
  }
  
  return res.json()
}

export default async function PostsPage() {
  try {
    const posts = await getPosts()
    return (
      <div>
        {posts.map(post => (
          <article key={post.id}>{post.title}</article>
        ))}
      </div>
    )
  } catch (error) {
    return <div>加载文章失败</div>
  }
}

使用 Suspense 和 Error Boundary

tsx
// app/posts/page.tsx
import { Suspense } from 'react'
import { ErrorBoundary } from '@/components/ErrorBoundary'

async function Posts() {
  const posts = await getPosts()
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  )
}

export default function PostsPage() {
  return (
    <ErrorBoundary fallback={<div>加载失败</div>}>
      <Suspense fallback={<div>加载中...</div>}>
        <Posts />
      </Suspense>
    </ErrorBoundary>
  )
}

🔍 错误日志和监控

错误日志记录

tsx
// lib/logger.ts
export function logError(error: Error, context?: any) {
  console.error('Error:', {
    message: error.message,
    stack: error.stack,
    context,
    timestamp: new Date().toISOString(),
  })
  
  // 发送到错误监控服务
  if (process.env.NODE_ENV === 'production') {
    // 例如:Sentry, LogRocket 等
  }
}

使用:

tsx
// app/error.tsx
'use client'

import { useEffect } from 'react'
import { logError } from '@/lib/logger'

export default function Error({ error, reset }) {
  useEffect(() => {
    logError(error, { page: 'home' })
  }, [error])

  return (
    <div>
      <h2>出错了</h2>
      <button onClick={reset}>重试</button>
    </div>
  )
}

📚 最佳实践

1. 分层错误处理

tsx
// 全局错误
// app/global-error.tsx

// 布局错误
// app/layout-error.tsx

// 页面错误
// app/page-error.tsx

// 组件错误
// components/ErrorBoundary.tsx

2. 友好的错误提示

tsx
'use client'

export default function Error({ error, reset }) {
  const getErrorMessage = (error: Error) => {
    if (error.message.includes('network')) {
      return '网络连接失败,请检查您的网络'
    }
    if (error.message.includes('timeout')) {
      return '请求超时,请稍后重试'
    }
    return '出现了一些问题,请稍后重试'
  }

  return (
    <div>
      <h2>{getErrorMessage(error)}</h2>
      <button onClick={reset}>重试</button>
    </div>
  )
}

3. 错误恢复策略

tsx
'use client'

import { useState } from 'react'

export default function Error({ error, reset }) {
  const [retryCount, setRetryCount] = useState(0)

  const handleRetry = () => {
    setRetryCount(prev => prev + 1)
    reset()
  }

  if (retryCount >= 3) {
    return (
      <div>
        <h2>多次重试失败</h2>
        <p>请联系技术支持</p>
      </div>
    )
  }

  return (
    <div>
      <h2>出错了</h2>
      <button onClick={handleRetry}>
        重试 ({retryCount}/3)
      </button>
    </div>
  )
}

🔗 相关资源


下一步:学习 Next.js 服务端渲染 (SSR)