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 文件的错误隔离机制:
- 自动捕获:自动捕获子组件的错误
- 错误隔离:错误不会影响其他组件
- 嵌套支持:支持嵌套的错误边界
- 错误恢复:提供 reset() 函数恢复错误状态
- 用户友好:显示友好的错误信息
reset() 函数的作用:
- 重新渲染:重新渲染错误边界内的组件
- 状态重置:重置组件的状态
- 错误恢复:尝试恢复错误状态
- 用户控制:让用户主动触发重试
最佳实践:
- 错误分类:根据错误类型提供不同的处理策略
- 用户友好:显示清晰的错误信息和操作建议
- 错误监控:记录错误信息用于调试和监控
- 重试机制:提供合理的重试策略
- 错误恢复:提供多种错误恢复选项
这些技术让 Next.js 应用能够优雅地处理错误,提供更好的用户体验。