Next.js Error Handling

Overview

Next.js provides comprehensive error handling mechanisms, including error boundaries, custom error pages, and global error handling, helping developers gracefully handle various error scenarios in applications.

Error Types

1. Client-Side Errors

  • Component rendering errors
  • Event handler errors
  • Asynchronous operation errors

2. Server-Side Errors

  • Data fetching errors
  • API route errors
  • Server rendering errors

3. Routing Errors

  • 404 page not found
  • 500 server errors

The error.tsx File

Basic Usage

// 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>
  )
}

Nested Error Handling

// 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 Handling

global-error.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>
  )
}

Custom 404 Page

not-found.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>
  )
}

Triggering 404

// 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>
}

Error Boundaries

React Error Boundary

// 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
  }
}

Usage:

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

API Route Error Handling

Basic Error Handling

// 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 }
    )
  }
}

Custom Error Classes

// 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 }
    )
  }
}

Data Fetching Error Handling

Server Component Error Handling

// 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>
  }
}

Using Suspense and Error Boundary

// 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>
  )
}

Error Logging and Monitoring

Error Logging

// 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 等
  }
}

Usage:

// 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>
  )
}

Best Practices

1. Layered Error Handling

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

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

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

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

2. User-Friendly Error Messages

'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. Error Recovery Strategies

'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>
  )
}

Previous Chapter: Metadata | Next Chapter: Server-Side Rendering (SSR)