Q20: 如何优雅地处理错误?error.js 文件是如何实现错误隔离的?如何使用 reset() 函数?

29 阅读5分钟

Next.js 面试题详细答案 - Q20

Q20: 如何优雅地处理错误?error.js 文件是如何实现错误隔离的?如何使用 reset() 函数?

error.js 文件错误隔离

1. 基本用法
// app/blog/error.js
'use client'
export default function BlogError({ error, reset }) {
  return (
    <div className="error">
      <h2>博客加载失败</h2>
      <p>错误信息: {error.message}</p>
      <button onClick={() => reset()}>重试</button>
    </div>
  )
}

// app/blog/page.js
async function BlogPage() {
  const posts = await fetch('/api/posts')

  if (!posts.ok) {
    throw new Error('Failed to fetch posts')
  }

  const data = await posts.json()

  return (
    <div>
      <h1>博客</h1>
      <ul>
        {data.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

export default BlogPage
2. 嵌套错误边界
// app/layout.js - 根布局
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <header>全局头部</header>
        {children}
        <footer>全局底部</footer>
      </body>
    </html>
  )
}

// app/error.js - 全局错误处理
'use client'
export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <div className="global-error">
          <h1>应用错误</h1>
          <p>发生了严重错误: {error.message}</p>
          <button onClick={() => reset()}>重新加载</button>
        </div>
      </body>
    </html>
  )
}

// app/blog/error.js - 博客错误处理
'use client'
export default function BlogError({ error, reset }) {
  return (
    <div className="blog-error">
      <h2>博客加载失败</h2>
      <p>错误信息: {error.message}</p>
      <div className="error-actions">
        <button onClick={() => reset()}>重试</button>
        <a href="/">返回首页</a>
      </div>
    </div>
  )
}

// app/blog/[slug]/error.js - 文章错误处理
'use client'
export default function PostError({ error, reset }) {
  return (
    <div className="post-error">
      <h2>文章加载失败</h2>
      <p>错误信息: {error.message}</p>
      <div className="error-actions">
        <button onClick={() => reset()}>重试</button>
        <a href="/blog">返回博客列表</a>
      </div>
    </div>
  )
}

错误处理策略

1. 不同类型错误的处理
// app/products/error.js
'use client'
import { useEffect } from 'react'

export default function ProductsError({ error, reset }) {
  useEffect(() => {
    // 记录错误到监控服务
    console.error('Products page error:', error)

    // 发送错误报告
    fetch('/api/error-report', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        error: error.message,
        stack: error.stack,
        url: window.location.href,
        timestamp: new Date().toISOString(),
      }),
    })
  }, [error])

  const getErrorMessage = (error) => {
    if (error.message.includes('Failed to fetch')) {
      return '网络连接失败,请检查网络设置'
    }
    if (error.message.includes('404')) {
      return '产品不存在或已被删除'
    }
    if (error.message.includes('500')) {
      return '服务器内部错误,请稍后重试'
    }
    return '加载产品时发生错误'
  }

  return (
    <div className="products-error">
      <div className="error-icon">⚠️</div>
      <h2>产品加载失败</h2>
      <p>{getErrorMessage(error)}</p>

      <div className="error-actions">
        <button onClick={() => reset()}>重试</button>
        <button onClick={() => window.location.reload()}>刷新页面</button>
        <a href="/">返回首页</a>
      </div>

      <div className="error-details">
        <details>
          <summary>错误详情</summary>
          <pre>{error.message}</pre>
        </details>
      </div>
    </div>
  )
}
2. 错误恢复机制
// app/dashboard/error.js
'use client'
import { useState } from 'react'

export default function DashboardError({ error, reset }) {
  const [isRetrying, setIsRetrying] = useState(false)

  const handleRetry = async () => {
    setIsRetrying(true)
    try {
      // 执行重试逻辑
      await reset()
    } catch (retryError) {
      console.error('Retry failed:', retryError)
    } finally {
      setIsRetrying(false)
    }
  }

  const handleGoBack = () => {
    window.history.back()
  }

  return (
    <div className="dashboard-error">
      <div className="error-content">
        <h2>仪表盘加载失败</h2>
        <p>抱歉,加载仪表盘时发生了错误</p>

        <div className="error-info">
          <p>
            <strong>错误类型:</strong> {error.name}
          </p>
          <p>
            <strong>错误信息:</strong> {error.message}
          </p>
          <p>
            <strong>发生时间:</strong> {new Date().toLocaleString()}
          </p>
        </div>

        <div className="error-actions">
          <button
            onClick={handleRetry}
            disabled={isRetrying}
            className="retry-button"
          >
            {isRetrying ? '重试中...' : '重试'}
          </button>

          <button onClick={handleGoBack} className="back-button">
            返回上页
          </button>

          <a href="/dashboard" className="home-link">
            返回仪表盘
          </a>
        </div>
      </div>
    </div>
  )
}

reset() 函数使用

1. 基本用法
// app/blog/error.js
'use client'
export default function BlogError({ error, reset }) {
  const handleReset = () => {
    // reset() 函数会重新渲染错误边界内的组件
    reset()
  }

  return (
    <div className="error">
      <h2>博客加载失败</h2>
      <p>错误信息: {error.message}</p>
      <button onClick={handleReset}>重试</button>
    </div>
  )
}
2. 带状态的重试
// app/products/error.js
'use client'
import { useState } from 'react'

export default function ProductsError({ error, reset }) {
  const [retryCount, setRetryCount] = useState(0)
  const [isRetrying, setIsRetrying] = useState(false)

  const handleRetry = async () => {
    setIsRetrying(true)
    setRetryCount((prev) => prev + 1)

    try {
      // 执行重试
      await reset()
    } catch (retryError) {
      console.error('Retry failed:', retryError)
    } finally {
      setIsRetrying(false)
    }
  }

  return (
    <div className="products-error">
      <h2>产品加载失败</h2>
      <p>错误信息: {error.message}</p>

      {retryCount > 0 && <p>已重试 {retryCount} 次</p>}

      <button onClick={handleRetry} disabled={isRetrying}>
        {isRetrying ? '重试中...' : '重试'}
      </button>

      {retryCount >= 3 && (
        <div className="max-retries">
          <p>重试次数过多,请稍后再试</p>
          <a href="/">返回首页</a>
        </div>
      )}
    </div>
  )
}
3. 条件重试
// app/api/error.js
'use client'
import { useState, useEffect } from 'react'

export default function ApiError({ error, reset }) {
  const [canRetry, setCanRetry] = useState(true)
  const [retryDelay, setRetryDelay] = useState(0)

  useEffect(() => {
    // 检查错误类型,决定是否可以重试
    if (error.message.includes('404') || error.message.includes('403')) {
      setCanRetry(false)
    } else {
      // 设置重试延迟
      setRetryDelay(5000)
      const timer = setTimeout(() => {
        setRetryDelay(0)
      }, 5000)

      return () => clearTimeout(timer)
    }
  }, [error])

  const handleRetry = () => {
    if (canRetry && retryDelay === 0) {
      reset()
    }
  }

  return (
    <div className="api-error">
      <h2>API 请求失败</h2>
      <p>错误信息: {error.message}</p>

      {canRetry ? (
        <div>
          {retryDelay > 0 ? (
            <p>请等待 {retryDelay / 1000} 秒后重试</p>
          ) : (
            <button onClick={handleRetry}>重试</button>
          )}
        </div>
      ) : (
        <div>
          <p>此错误无法通过重试解决</p>
          <a href="/">返回首页</a>
        </div>
      )}
    </div>
  )
}

错误监控和报告

1. 错误监控集成
// app/error.js
'use client'
import { useEffect } from 'react'

export default function GlobalError({ error, reset }) {
  useEffect(() => {
    // 发送错误到监控服务
    const reportError = async () => {
      try {
        await fetch('/api/error-report', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            error: {
              message: error.message,
              stack: error.stack,
              name: error.name,
            },
            url: window.location.href,
            userAgent: navigator.userAgent,
            timestamp: new Date().toISOString(),
            userId: getUserId(), // 获取用户ID
          }),
        })
      } catch (reportError) {
        console.error('Failed to report error:', reportError)
      }
    }

    reportError()
  }, [error])

  return (
    <html>
      <body>
        <div className="global-error">
          <h1>应用错误</h1>
          <p>发生了严重错误,我们正在处理</p>
          <button onClick={() => reset()}>重新加载</button>
        </div>
      </body>
    </html>
  )
}

function getUserId() {
  // 从 cookie 或 localStorage 获取用户ID
  return (
    document.cookie
      .split('; ')
      .find((row) => row.startsWith('user-id='))
      ?.split('=')[1] || 'anonymous'
  )
}
2. 错误分类处理
// app/blog/error.js
'use client'
import { useEffect } from 'react'

export default function BlogError({ error, reset }) {
  useEffect(() => {
    // 根据错误类型进行分类处理
    const errorType = classifyError(error)

    switch (errorType) {
      case 'NETWORK_ERROR':
        // 网络错误,可以重试
        break
      case 'AUTH_ERROR':
        // 认证错误,重定向到登录页
        window.location.href = '/login'
        break
      case 'NOT_FOUND':
        // 404错误,重定向到404页面
        window.location.href = '/404'
        break
      case 'SERVER_ERROR':
        // 服务器错误,记录并显示错误页面
        console.error('Server error:', error)
        break
      default:
        // 其他错误,记录并显示通用错误页面
        console.error('Unknown error:', error)
    }
  }, [error])

  const classifyError = (error) => {
    if (error.message.includes('Failed to fetch')) {
      return 'NETWORK_ERROR'
    }
    if (error.message.includes('401') || error.message.includes('403')) {
      return 'AUTH_ERROR'
    }
    if (error.message.includes('404')) {
      return 'NOT_FOUND'
    }
    if (error.message.includes('500')) {
      return 'SERVER_ERROR'
    }
    return 'UNKNOWN_ERROR'
  }

  const getErrorMessage = (error) => {
    const errorType = classifyError(error)

    switch (errorType) {
      case 'NETWORK_ERROR':
        return '网络连接失败,请检查网络设置'
      case 'AUTH_ERROR':
        return '认证失败,请重新登录'
      case 'NOT_FOUND':
        return '请求的资源不存在'
      case 'SERVER_ERROR':
        return '服务器内部错误,请稍后重试'
      default:
        return '发生了未知错误'
    }
  }

  return (
    <div className="blog-error">
      <h2>博客加载失败</h2>
      <p>{getErrorMessage(error)}</p>

      <div className="error-actions">
        <button onClick={() => reset()}>重试</button>
        <a href="/">返回首页</a>
      </div>
    </div>
  )
}

最佳实践

1. 错误边界设计
// app/products/error.js
'use client'
import { useState } from 'react'

export default function ProductsError({ error, reset }) {
  const [showDetails, setShowDetails] = useState(false)

  return (
    <div className="products-error">
      <div className="error-header">
        <h2>产品加载失败</h2>
        <p>抱歉,加载产品时发生了错误</p>
      </div>

      <div className="error-content">
        <div className="error-message">
          <p>{error.message}</p>
        </div>

        <div className="error-actions">
          <button onClick={() => reset()}>重试</button>
          <button onClick={() => window.location.reload()}>刷新页面</button>
          <a href="/">返回首页</a>
        </div>

        <div className="error-details">
          <button onClick={() => setShowDetails(!showDetails)}>
            {showDetails ? '隐藏' : '显示'}错误详情
          </button>

          {showDetails && (
            <div className="error-stack">
              <pre>{error.stack}</pre>
            </div>
          )}
        </div>
      </div>
    </div>
  )
}
2. 错误恢复策略
// app/dashboard/error.js
'use client'
import { useState, useEffect } from 'react'

export default function DashboardError({ error, reset }) {
  const [retryCount, setRetryCount] = useState(0)
  const [isRetrying, setIsRetrying] = useState(false)

  const handleRetry = async () => {
    setIsRetrying(true)
    setRetryCount((prev) => prev + 1)

    try {
      await reset()
    } catch (retryError) {
      console.error('Retry failed:', retryError)
    } finally {
      setIsRetrying(false)
    }
  }

  const handleGoBack = () => {
    window.history.back()
  }

  const handleGoHome = () => {
    window.location.href = '/'
  }

  return (
    <div className="dashboard-error">
      <div className="error-content">
        <h2>仪表盘加载失败</h2>
        <p>抱歉,加载仪表盘时发生了错误</p>

        <div className="error-info">
          <p>
            <strong>错误信息:</strong> {error.message}
          </p>
          <p>
            <strong>重试次数:</strong> {retryCount}
          </p>
        </div>

        <div className="error-actions">
          <button
            onClick={handleRetry}
            disabled={isRetrying || retryCount >= 3}
            className="retry-button"
          >
            {isRetrying ? '重试中...' : '重试'}
          </button>

          <button onClick={handleGoBack} className="back-button">
            返回上页
          </button>

          <button onClick={handleGoHome} className="home-button">
            返回首页
          </button>
        </div>

        {retryCount >= 3 && (
          <div className="max-retries">
            <p>重试次数过多,请稍后再试或联系客服</p>
            <a href="/contact">联系客服</a>
          </div>
        )}
      </div>
    </div>
  )
}

总结

error.js 文件的错误隔离机制:

  1. 自动捕获:自动捕获子组件的错误
  2. 错误隔离:错误不会影响其他组件
  3. 嵌套支持:支持嵌套的错误边界
  4. 错误恢复:提供 reset() 函数恢复错误状态
  5. 用户友好:显示友好的错误信息

reset() 函数的作用:

  1. 重新渲染:重新渲染错误边界内的组件
  2. 状态重置:重置组件的状态
  3. 错误恢复:尝试恢复错误状态
  4. 用户控制:让用户主动触发重试

最佳实践:

  1. 错误分类:根据错误类型提供不同的处理策略
  2. 用户友好:显示清晰的错误信息和操作建议
  3. 错误监控:记录错误信息用于调试和监控
  4. 重试机制:提供合理的重试策略
  5. 错误恢复:提供多种错误恢复选项

这些技术让 Next.js 应用能够优雅地处理错误,提供更好的用户体验。