切换语言
切换主题

Next.js Error Boundary 完全指南:优雅处理运行时错误的 5 个关键技巧

凌晨3点,手机震了。睁眼一看,运营团队群里已经炸了:“首页打不开了!全是白屏!”

打开监控平台,心一凉——某个第三方组件挂了,直接把整个页面拖下水。用户看到的,就是一片惨白,连个”出错了”的提示都没有。

说实话,这种事我经历过不止一次。你可能也遇到过类似的场景:生产环境跑得好好的页面,突然因为一个数据格式不对、一个API超时,整个应用就崩了。传统的 try-catch 根本管不到 React 组件渲染这一层,结果就是用户盯着白屏发呆,然后默默关掉页面。

根据用户体验研究,页面白屏会导致超过 80% 的用户直接流失。这数字挺吓人的。

好在 Next.js 提供了 Error Boundary 机制,能让你优雅地处理这些运行时错误。不光能避免白屏,还能给用户一个友好的降级界面,甚至提供”重试”按钮让他们自己恢复。这篇文章就来聊聊 Next.js Error Boundary 的完整用法——从基础的 error.tsx 到全局兜底的 global-error.tsx,再到 Server Components 的特殊处理。

读完这篇,你能知道怎么让应用在出错时更优雅,避免那种半夜被叫醒修bug的惨况。

为什么需要 Error Boundary?传统错误处理的局限

刚开始用 React 的时候,我以为 try-catch 能搞定所有错误。结果很快就被现实打脸了。

try-catch 的三个硬伤

先说第一个:它只能捕获同步代码的错误。你在 try 块里写 JSON.parse(badData),能抓到。但如果是组件渲染过程中报错?抱歉,抓不到。

第二个更坑:事件处理器里的异步错误。比如你在点击事件里调用一个 API,API 挂了,try-catch 也管不了。为啥?因为异步代码执行时,try-catch 的上下文早就结束了。

第三个是最致命的:React 组件渲染错误。你的组件 return 语句里访问了一个 undefined 的属性,页面直接白屏。try-catch 在这里完全没用。

React Error Boundary 的工作原理

React 其实很早就意识到这个问题了,那引入了 Error Boundary 机制。原理挺简单:组件树就像俄罗斯套娃,错误会从内层一层层往外”冒泡”,直到碰到最近的 Error Boundary 组件为止。

传统的做法是写一个类组件,实现 componentDidCatchgetDerivedStateFromError 这俩生命周期方法。说实话,每次都要写类组件,挺烦的。而且很多人现在习惯用函数组件了,这俩玩意儿根本用不了。

Next.js 的优雅解决方案

Next.js 13 引入 App Router 之后,对 Error Boundary 做了一层封装,变得超简单。你只需要在路由目录里创建一个 error.tsx 文件,它就自动变成该路由的错误边界。不用写类组件,不用自己管理状态,Next.js 全帮你搞定了。

还有个关键点:Next.js 的 Error Boundary 能同时处理服务端和客户端的错误。Server Components 在服务器渲染时报错,也会被最近的 error.tsx 捕获。这在传统 React 里是做不到的。

唯一要注意的是,error.tsx 文件本身必须是客户端组件,开头要加 'use client' 标记。为啥?因为它需要用 React hooks 来处理错误状态和恢复逻辑,而 hooks 只能在客户端跑。

Facebook Messenger 就是个经典案例。他们把侧边栏、对话框、消息输入这些区域分别用 Error Boundary 包起来。一个区域崩了,其他区域照常工作。用户可能压根儿没意识到出了问题。

这就是 Error Boundary 的核心价值:不让局部错误变成全局灾难。

error.tsx 使用方法 - 局部错误边界

好,现在来点实战的。error.tsx 到底怎么写?

基本结构:5 分钟上手

在任意路由目录下创建 error.tsx,粘贴这段代码:

'use client'

import { useEffect } from 'react'

export default function Error({
  error,
  reset
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // 把错误上报到监控平台,比如 Sentry
    console.error('捕获到错误:', error)
  }, [error])

  return (
    <div className="flex flex-col items-center justify-center min-h-screen p-4">
      <h2 className="text-2xl font-bold mb-4">哎呀,出了点问题</h2>
      <p className="text-gray-600 mb-4">
        {error.message || '页面加载失败了'}
      </p>
      <button
        onClick={() => reset()}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        重试一下
      </button>
    </div>
  )
}

几个关键点:

  1. ‘use client’ 不能漏:开头这行必须有,否则 Next.js 会报错
  2. error 对象:包含错误信息和堆栈,还有个 digest 字段是 Next.js 15 新加的,用来做错误追踪
  3. reset 函数:点击后会重新渲染错误边界内的内容,给用户一个自救机会

错误冒泡机制:像电梯一样逐层向上

这个机制刚开始确实有点绕。我画个目录结构你就明白了:

app/
├── layout.tsx          # 根布局
├── error.tsx           # 捕获根路由下的错误 (A)
├── page.tsx            # 首页
├── dashboard/
│   ├── layout.tsx      # dashboard 布局
│   ├── error.tsx       # 捕获 dashboard 下的错误 (B)
│   └── page.tsx        # dashboard 页面
└── profile/
    └── page.tsx        # profile 页面

假设 dashboard/page.tsx 渲染时报错了,错误会被谁捕获?答案是 (B)——最近的父级 error.tsx。

profile/page.tsx 报错呢?profile 目录下没有 error.tsx,错误会继续往上冒泡,被 (A) 捕获。

有个坑要注意:error.tsx 捕获不了同级 layout.tsx 的错误。为啥?因为错误边界本身就是包在 layout 里的,layout 挂了,错误边界还没加载呢。如果要捕获 dashboard/layout.tsx 的错误,你得在 app/error.tsx 里处理。

reset() 的正确用法

reset 函数听起来很神奇,其实就是重新渲染一遍错误组件的子树。适用场景是暂时性错误,比如:

  • API 请求超时(重试可能成功)
  • 网络抖动导致资源加载失败
  • 用户输入触发的边界条件

但如果是代码 bug,比如你访问了 undefined.property,按多少次重试都没用。这种情况,你得在监控平台看到错误后,赶紧修代码发版。

我见过有的团队在 reset 逻辑里加了个计数器,重试超过3次就不再显示”重试”按钮,而是引导用户刷新页面或联系客服。这招挺实用的:

'use client'

import { useEffect, useState } from 'react'

export default function Error({ error, reset }: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  const [retryCount, setRetryCount] = useState(0)

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

  return (
    <div>
      <h2>出错了</h2>
      {retryCount < 3 ? (
        <button onClick={handleReset}>
          重试 ({retryCount}/3)
        </button>
      ) : (
        <p>多次重试失败,请刷新页面或<a href="/contact">联系我们</a></p>
      )}
    </div>
  )
}
40%
提供重试按钮能让约 40% 的暂时性错误自动恢复

global-error.tsx - 全局错误兜底方案

error.tsx 很强大,但还有个漏洞:它捕获不了根布局 app/layout.tsx 的错误。这时候就该 global-error.tsx 出场了。

什么时候用 global-error.tsx?

说实话,这个文件在生产环境很少被触发。它主要处理两种灾难性场景:

  1. 根 layout.tsx 初始化失败(比如全局状态管理库挂了)
  2. 所有 error.tsx 都没捕获到的”漏网之鱼”

我把它理解成最后的安全网——你希望永远用不上,但必须得有。

global-error.tsx 的特殊性

跟普通 error.tsx 相比,global-error.tsx 有个关键区别:它必须包含完整的 HTML 结构,也就是 <html><body> 标签。

为啥?因为它会完全替换掉根 layout.tsx。你的根布局挂了,整个页面框架都没了,global-error.tsx 得从头搭建一个最小可用的页面。

完整代码长这样:

'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          minHeight: '100vh',
          padding: '20px',
          fontFamily: 'system-ui, sans-serif'
        }}>
          <h1>应用遇到了严重问题</h1>
          <p style={{ color: '#666', marginBottom: '20px' }}>
            {process.env.NODE_ENV === 'development'
              ? error.message
              : '我们正在处理这个问题,请稍后再试'}
          </p>
          <button
            onClick={() => reset()}
            style={{
              padding: '10px 20px',
              background: '#0070f3',
              color: 'white',
              border: 'none',
              borderRadius: '5px',
              cursor: 'pointer'
            }}
          >
            重新加载应用
          </button>
        </div>
      </body>
    </html>
  )
}

你会注意到我用了内联样式,而不是 Tailwind 或 CSS 模块。原因很简单:这时候你的样式系统可能都还没加载,得用最原始的方式保证页面能看。

开发环境 vs 生产环境

有个细节值得注意:global-error.tsx 只在生产环境生效。开发环境下,Next.js 会继续显示那个红色的错误堆栈页面,方便你调试。

生产环境里,我建议隐藏技术性的错误信息,只给用户看友好提示。上面代码里有个 process.env.NODE_ENV 判断就是干这个的。用户不关心”TypeError: Cannot read property ‘map’ of undefined”,他们只想知道”能不能用”和”什么时候能修好”。

要不要加 global-error.tsx?

我的建议是:加。虽然它被触发的概率很低,但一旦触发就是大事故。有了这个兜底方案,至少能保证用户看到的是个体面的错误页面,而不是浏览器默认的”无法访问此网站”。

就像买保险——你不希望出事,但出了事有保障总比没有强。

Server Components 错误处理的特殊考虑

Next.js 13+ 的 Server Components 给错误处理带来了新挑战。服务端和客户端的错误,处理方式不太一样。

Server Components 的错误会去哪?

刚开始接触 Server Components 时,我确实有点懵。服务端组件在服务器上渲染,如果出错了,客户端的 error.tsx 能捕获到吗?

答案是:能。Next.js 会把服务端的错误信息传递到客户端,触发最近的 error.tsx。但有个重要的安全机制:生产环境下,错误信息会被脱敏,防止泄露敏感的服务器信息。

比如你的数据库连接失败,开发环境会显示完整的错误堆栈,但生产环境用户只会看到”加载失败”这种通用提示。

预期错误 vs 意外错误

这个概念挺重要,官方文档专门强调了。你得区分两种错误:

预期错误:业务逻辑范围内的错误,应该被显式处理

  • 表单验证失败(用户输入格式不对)
  • API 返回 404(数据不存在)
  • 权限不足(用户没登录)

意外错误:代码 bug 或系统级别的异常,应该交给 Error Boundary

  • 数据库连接失败
  • 第三方服务挂了
  • 代码访问了 undefined 的属性

对于预期错误,你应该在 Server Action 或数据获取函数里用 try-catch 处理,然后返回错误信息给组件:

// app/actions.ts
'use server'

export async function createUser(formData: FormData) {
  const email = formData.get('email') as string

  // 预期错误:邮箱格式不对
  if (!email.includes('@')) {
    return { error: '请输入有效的邮箱地址' }
  }

  try {
    await db.user.create({ email })
    return { success: true }
  } catch (error) {
    // 意外错误:数据库挂了,抛出让 Error Boundary 处理
    throw new Error('创建用户失败')
  }
}

对于意外错误,直接 throw,让它冒泡到最近的 error.tsx。

数据获取中的错误处理

Server Components 里做数据获取,我一般这样处理:

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')

  // 预期错误:API 返回错误状态码
  if (!res.ok) {
    // 根据错误类型决定是显式处理还是抛出
    if (res.status === 404) {
      return { posts: [], error: '暂无数据' }
    }
    // 服务器错误,抛出让 Error Boundary 处理
    throw new Error('获取数据失败')
  }

  return { posts: await res.json() }
}

export default async function PostsPage() {
  const { posts, error } = await getPosts()

  // 显式渲染错误状态
  if (error) {
    return <div>暂无文章</div>
  }

  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

这样做的好处是,用户体验更友好。“暂无数据”不需要触发错误页面,只有真正的系统错误才会显示 error.tsx 的兜底 UI。

error.digest 的妙用

Next.js 15 给 error 对象加了个 digest 字段,这是个自动生成的唯一标识。

它有啥用?想象这个场景:用户看到错误页面,截图发给客服说”页面打不开”。客服拿着这个 digest 去查日志,能精准定位到是哪次请求、什么时候、什么错误。

在 error.tsx 里可以这样用:

'use client'

export default function Error({ error }: { error: Error & { digest?: string }}) {
  return (
    <div>
      <h2>出错了</h2>
      <p>错误编号:{error.digest}</p>
      <p>请联系客服并提供上述编号</p>
    </div>
  )
}

配合 Sentry 或其他监控平台,这个 digest 能让错误追踪效率提升好几倍。

生产环境最佳实践

前面讲了怎么用,现在聊聊怎么用好。这些是我踩过坑之后总结的经验。

1. 细粒度错误边界设计

别只在根目录放一个 error.tsx 就完事了。关键功能区域最好单独设置错误边界。

举个例子,电商网站可以这样分:

app/
├── error.tsx                    # 兜底
├── (shop)/
│   ├── products/
│   │   └── error.tsx           # 商品列表出错不影响其他区域
│   ├── cart/
│   │   └── error.tsx           # 购物车出错不影响浏览商品
│   └── checkout/
│       └── error.tsx           # 结账流程最关键,单独处理

这样的好处是,即使购物车组件崩了,用户还能继续浏览商品。不至于整个网站都不能用。

2. 错误监控和上报

error.tsx 的 useEffect 是个完美的上报时机:

'use client'

import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'

export default function Error({ error, reset }: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // 上报到 Sentry
    Sentry.captureException(error, {
      tags: {
        errorDigest: error.digest,
        errorBoundary: 'app-root'
      },
      extra: {
        userAgent: navigator.userAgent,
        timestamp: new Date().toISOString()
      }
    })
  }, [error])

  return (
    // 错误 UI...
  )
}

记得带上 error.digest 和用户环境信息,方便复现问题。

我见过有的团队还会记录用户最近的操作路径(比如最后访问的5个页面),这对排查问题帮助很大。

3. 用户友好的错误 UI

技术人员喜欢看堆栈信息,但用户不care这些。他们想知道的是:

  • 发生了什么?(用通俗的话说)
  • 能不能解决?(提供明确的操作)
  • 我的数据丢了吗?(说明影响范围)

一个好的错误 UI 应该是这样的:

return (
  <div className="error-container">
    <h2>页面加载失败了</h2>
    <p>可能是网络不稳定,或者我们的服务器在打盹</p>

    <div className="actions">
      <button onClick={reset}>重试一下</button>
      <a href="/">返回首页</a>
      <a href="/help">联系客服</a>
    </div>

    <details className="error-details">
      <summary>技术信息(可选)</summary>
      <code>{error.digest}</code>
    </details>
  </div>
)

用轻松的语气,避免让用户焦虑。“服务器在打盹”比”500 Internal Server Error”友好得多。

4. 智能重试策略

前面提到过限制重试次数,这里再补充几个技巧:

  • 延迟重试:不要立即 reset,等1-2秒,给服务器喘息时间
  • 指数退避:第一次等1秒,第二次等2秒,第三次等4秒
  • 区分错误类型:网络错误建议重试,代码错误直接提示联系技术支持
const [retryCount, setRetryCount] = useState(0)
const [isRetrying, setIsRetrying] = useState(false)

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

  // 指数退避:2^retryCount 秒
  await new Promise(resolve =>
    setTimeout(resolve, Math.pow(2, retryCount) * 1000)
  )

  setIsRetrying(false)
  reset()
}

5. 环境差异化处理

开发环境和生产环境的错误展示应该不一样:

const isDev = process.env.NODE_ENV === 'development'

return (
  <div>
    <h2>{isDev ? error.message : '出了点问题'}</h2>

    {isDev && (
      <pre>
        <code>{error.stack}</code>
      </pre>
    )}

    {!isDev && (
      <p>我们已经记录了这个问题,会尽快修复</p>
    )}
  </div>
)

开发环境给你完整堆栈,方便调试。生产环境只显示友好提示,不泄露技术细节。

6. 不要过度使用

最后提醒一点:Error Boundary 是兜底方案,不是主要的错误处理手段。

能用 try-catch 处理的预期错误,就别抛给 Error Boundary。能在组件内部优雅降级的,就别触发错误页面。

比如用户头像加载失败,显示默认头像就行了,不需要整个个人资料页面都挂掉。

Error Boundary 应该留给那些真正意外的、无法在局部处理的错误。

结论

说了这么多,核心其实就三点:

第一,Error Boundary 不是可选项,是必需品。页面白屏导致的用户流失,比你想象的严重得多。花点时间设置好错误边界,能避免很多半夜被叫醒的情况。

第二,分层处理很关键。error.tsx 负责局部错误,global-error.tsx 做全局兜底,Server Components 里区分预期错误和意外错误。该显式处理的就显式处理,该交给错误边界的就别拦着。

第三,用户体验优先。技术细节留给监控平台,给用户看的永远是友好、可操作的提示。“重试”按钮能解决 40% 的暂时性错误,这个投入产出比相当高。

现在就去你的 Next.js 项目里加个 error.tsx 吧。从根目录开始,再逐步给关键功能区域加上错误边界。配上 Sentry 之类的监控工具,你会发现应用稳定性肉眼可见地提升。

对了,别忘了 global-error.tsx。虽然很少触发,但它就像安全带——你不希望用上,但必须得有。

在 Next.js 中实现 Error Boundary

为 Next.js 应用添加错误边界,优雅处理运行时错误

  1. 1

    步骤1: 创建 error.tsx 文件

    在 app 目录或任何路由目录下创建 error.tsx 文件,添加 'use client' 指令
  2. 2

    步骤2: 实现错误处理组件

    定义 Error 组件,接收 error 和 reset 参数,设计友好的错误 UI
  3. 3

    步骤3: 添加错误上报

    在 useEffect 中将错误上报到 Sentry 等监控平台,记录 error.digest
  4. 4

    步骤4: 实现智能重试

    添加重试按钮,限制重试次数,对于暂时性错误提供自动恢复机制
  5. 5

    步骤5: 创建 global-error.tsx

    在 app 目录创建 global-error.tsx 作为最后的兜底方案,包含完整 HTML 结构
  6. 6

    步骤6: 区分错误类型

    Server Components 中区分预期错误(显式处理)和意外错误(交给 Error Boundary)

常见问题

error.tsx 和 global-error.tsx 有什么区别?
error.tsx 捕获路由段级别的错误,但捕获不了同级 layout.tsx 的错误。global-error.tsx 是最后的兜底方案,能捕获根 layout.tsx 的错误,必须包含完整的 html 和 body 标签,只在生产环境生效。
为什么 error.tsx 必须是客户端组件?
因为 error.tsx 需要使用 React hooks(如 useEffect)来处理错误状态和恢复逻辑,而 hooks 只能在客户端组件中使用。所以必须在文件开头添加 'use client' 指令。
Server Components 的错误能被 error.tsx 捕获吗?
能。Next.js 会把服务端的错误信息传递到客户端,触发最近的 error.tsx。但生产环境下错误信息会被脱敏,防止泄露敏感的服务器信息。
什么时候应该用 try-catch 而不是 Error Boundary?
预期的业务错误(如表单验证失败、API 返回 404、权限不足)应该用 try-catch 显式处理。Error Boundary 应该留给意外错误(如代码 bug、数据库连接失败、第三方服务挂掉)。
reset() 函数是如何工作的?
reset() 会重新渲染错误边界内的组件子树。它适用于暂时性错误(如网络超时、资源加载失败),重试可能成功。但对于代码 bug,重试无效,需要修复代码后发版。

14 分钟阅读 · 发布于: 2026年1月6日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章