React: 错误边界与调试

最后更新:2026-08-26

Tom 负责的电商后台在线上出现了"白屏"事故——用户反馈订单详情页打开后页面完全空白,控制台报错 Cannot read properties of undefined (reading 'name')。原因是一个子组件在渲染阶段抛出了未捕获的 JavaScript 错误,导致整个 React 组件树崩溃。Tom 需要在应用中建立错误边界体系,确保部分组件崩溃不影响整体页面可用性。


1. 你将学到



2. 概念图解

下面的图展示了 Error Boundary 的捕获流程和 Profiler 的分析链路:

100%
flowchart TD
    A[React 组件树] --> B[Error Boundary]

    B --> C{子组件渲染是否出错?}
    C -->|否| D[正常渲染]
    C -->|是| E[getDerivedStateFromError]

    E --> F[更新 state.hasError = true]
    F --> G[渲染 fallback UI]

    G --> H{是否点击重试?}
    H -->|是| I[重置 state]
    I --> A
    H -->|否| J[保持降级 UI]

    K[React DevTools Profiler] --> L[录制组件渲染]
    L --> M[火焰图分析]
    M --> N["识别耗时组件(黄色/红色)"]
    N --> O[React.memo / useMemo 优化]

    style B fill:#e3f2fd,stroke:#1565c0
    style E fill:#fff3e0,stroke:#e65100
    style G fill:#e8f5e9,stroke:#2e7d32
    style K fill:#f3e5f5,stroke:#7b1fa2


3. 一个真实场景

错误处理层级 捕获范围 恢复策略 适用场景
try/catch 单个异步操作 重试/降级 API 请求、Promise 操作
Error Boundary 子组件树渲染错误 降级 UI + 重试按钮 组件白屏防护
全局 unhandledrejection 未捕获的 Promise 错误 日志上报 兜底监控
window.onerror 全局同步错误 日志上报 兜底监控
React DevTools 开发时调试 定位问题 性能/渲染问题排查

Tom 的订单详情页包含以下组件结构:

TEXT 📖 仅展示
OrderPage
  ├── OrderHeader      (订单号、状态)
  ├── OrderItems       (商品列表)
  │   └── OrderItem    × N(单个商品,含价格计算)
  ├── ShippingInfo     (物流信息)
  └── PaymentInfo      (支付信息)

线上事故的原因是:某个订单的商品数据中 price 字段缺失,OrderItem 组件访问 item.price.toFixed(2) 时抛出了 TypeError,导致整个 OrderPage 白屏。

正确做法是:用 Error Boundary 包裹 OrderItems 区域,即使商品列表渲染失败,订单头部和支付信息依然可以正常显示。另外,Tom 还需要了解如何用 React DevTools Profiler 定位性能瓶颈。


(1) Error Boundary — 组件级安全网

Error Boundary 是 React 提供的声明式错误捕获机制。当子树中的任何组件在渲染阶段、生命周期方法或构造函数中抛出错误时,Error Boundary 可以捕获该错误并显示备用 UI,而不是让整个应用白屏。

注意: Error Boundary 目前只能用 class 组件实现(React 官方计划在未来版本中提供 Hook 版本)。

▶ 示例 1:通用 ErrorBoundary 组件

TSX 📖 仅展示
import { Component, ErrorInfo, ReactNode } from 'react'

interface ErrorBoundaryProps {
  children: ReactNode
  /** 自定义降级 UI */
  fallback?: ReactNode
  /** 错误回调(上报 Sentry 等) */
  onError?: (error: Error, errorInfo: ErrorInfo) => void
}

interface ErrorBoundaryState {
  hasError: boolean
  error: Error | null
}

class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
  constructor(props: ErrorBoundaryProps) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  // 静态方法:根据错误更新 state
  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error }
  }

  // 生命周期:捕获错误后做副作用(日志上报)
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary 捕获到错误:', error.message)
    console.error('组件栈:', errorInfo.componentStack)

    // 上报到错误监控服务
    if (this.props.onError) {
      this.props.onError(error, errorInfo)
    }

    // 实际项目可集成 Sentry:
    // Sentry.captureException(error, { extra: errorInfo })
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null })
  }

  render() {
    if (this.state.hasError) {
      // 使用自定义 fallback 或默认降级 UI
      if (this.props.fallback) {
        return this.props.fallback
      }

      return (
        <div
          role="alert"
          style={{
            padding: '32px 24px',
            margin: 16,
            background: '#fff2f0',
            border: '1px solid #ffccc7',
            borderRadius: 8,
            textAlign: 'center',
          }}
        >
          <h2 style={{ color: '#ff4d4f', margin: '0 0 12px' }}>
            组件出错了
          </h2>
          <p style={{ color: '#666', marginBottom: 8, fontSize: 14 }}>
            {this.state.error?.message || '发生了未知错误'}
          </p>
          <button
            onClick={this.handleReset}
            style={{
              padding: '6px 20px',
              background: '#ff4d4f',
              color: '#fff',
              border: 'none',
              borderRadius: 4,
              cursor: 'pointer',
              fontSize: 14,
            }}
          >
            重试
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

export default ErrorBoundary
逻辑代码 72 行(超过 40 行限制,仅展示)

在实际项目中使用 ErrorBoundary

TSX
// 分层包裹 — 每个独立区域有自己的 ErrorBoundary
function OrderPage({ orderId }: { orderId: string }) {
  return (
    <div>
      {/* 订单头信息:即使下面出错了也能正常显示 */}
      <ErrorBoundary fallback={<p>订单加载失败</p>}>
        <OrderHeader orderId={orderId} />
      </ErrorBoundary>

      {/* 商品列表:自己出错不影响其他区域 */}
      <ErrorBoundary
        onError={(err) => {
          // 上报商品列表渲染错误
          fetch('/api/log-error', {
            method: 'POST',
            body: JSON.stringify({ error: err.message, orderId }),
          })
        }}
      >
        <OrderItems orderId={orderId} />
      </ErrorBoundary>

      {/* 支付信息 */}
      <ErrorBoundary>
        <PaymentInfo orderId={orderId} />
      </ErrorBoundary>
    </div>
  )
}

▶ 示例 2:带错误恢复的 UserProfile 组件

实际业务中,有时候仅显示降级 UI 不够——用户可能需要刷新局部数据。下面是一个带"重试"功能的错误边界用法:

TSX 📖 仅展示
import { useState } from 'react'
import ErrorBoundary from './ErrorBoundary'

// 模拟会出错的数据获取
function fetchUserData(userId: number) {
  return fetch(`/api/users/${userId}`).then(res => {
    if (!res.ok) throw new Error('用户数据获取失败')
    return res.json()
  })
}

// 可能有渲染错误的数据展示组件
function UserInfo({ userId }: { userId: number }) {
  const [user, setUser] = useState<any>(null)
  const [loading, setLoading] = useState(true)

  useState(() => {
    fetchUserData(userId)
      .then(setUser)
      .finally(() => setLoading(false))
  })

  if (loading) return <p>加载中...</p>

  // 如果 user 数据结构异常,这里可能报错
  return (
    <div>
      <h3>{user.name}</h3>         {/* 可能:Cannot read properties of undefined */}
      <p>{user.profile.bio}</p>    {/* 可能:Cannot read properties of undefined */}
    </div>
  )
}

// 外层容器:带重试 key 机制
function UserProfile({ userId }: { userId: number }) {
  const [retryKey, setRetryKey] = useState(0)

  return (
    <ErrorBoundary
      key={retryKey}  // 改变 key 会卸载并重新挂载子树
      fallback={
        <div style={{ padding: 24, textAlign: 'center' }}>
          <p>用户信息加载异常</p>
          <button onClick={() => setRetryKey(k => k + 1)}>
            重试加载
          </button>
        </div>
      }
    >
      <UserInfo userId={userId} />
    </ErrorBoundary>
  )
}
逻辑代码 42 行(超过 40 行限制,仅展示)

关键技巧: key={retryKey} 让 ErrorBoundary 在点击重试时卸载并重新创建子树,从而重置所有子组件的状态。


(2) Error Boundary 不能捕获的错误

Error Boundary 不是万能的,以下四类错误它无法捕获:

错误类型 原因 解决方案
事件处理中的错误 事件处理器不在渲染阶段执行 用 try/catch 包裹事件处理逻辑
异步代码中的错误 setTimeout / Promise 回调不在 React 渲染周期内 用 try/catch 或 Promise.catch
服务端渲染(SSR)中的错误 Error Boundary 只在客户端生效 SSR 端用 try/catch 包裹渲染
Error Boundary 自身的错误 自身抛出错误无法捕获自己 在最外层再套一个 Error Boundary

事件处理 + 异步代码的正确错误处理

TSX
function PaymentForm() {
  async function handleSubmit() {
    try {
      const result = await submitPayment()
      // 成功处理
    } catch (error) {
      // 异步错误在这里捕获,Error Boundary 管不到这里
      console.error('支付失败:', error)
      // 显示错误 UI(比如设置 state)
      setError(error instanceof Error ? error.message : '支付失败')
    }
  }

  // 事件中的错误也要用 try/catch
  function handleClick() {
    try {
      processPayment()
    } catch (error) {
      setError('处理失败,请重试')
    }
  }
}

(3) 使用 React DevTools Profiler 分析性能

React DevTools 的 Profiler 标签是分析组件渲染性能的核心工具。它生成"火焰图",直观展示每个组件的渲染耗时。

使用步骤

TEXT 📖 仅展示
1. 打开浏览器 DevTools → Components 标签页
2. 切换到 Profiler 子标签
3. 点击蓝色录制按钮(开始录制)
4. 在页面上进行操作(点击、滚动等)
5. 点击停止按钮(结束录制)
6. 查看火焰图

火焰图解读方法

TEXT 📖 仅展示
┌────────────────────────────────────────────┐
│  App (0.3ms)                               │
│  ├── Navbar (0.2ms)                        │
│  ├── OrderPage (2.1ms)                     │
│  │   ├── OrderHeader (0.4ms)  ── 灰色     │
│  │   ├── OrderItems (1.5ms)   ── 黄色     │
│  │   │   └── OrderItem × 20 (各 0.3ms)     │
│  │   └── PaymentInfo (0.2ms) ── 灰色      │
│  └── Footer (0.1ms)                        │
└────────────────────────────────────────────┘

▶ 示例 3:用 Profiler 组件测量渲染耗时

React 内置的 <Profiler> 组件可以在代码中精确测量某个组件的渲染耗时,适合做性能指标的自动监控:

TSX 📖 仅展示
import { Profiler } from 'react'

type ProfilerPhase = 'mount' | 'update' | 'nested-update'

interface ProfileMetrics {
  id: string
  phase: ProfilerPhase
  actualDuration: number      // 本次渲染实际耗时(毫秒)
  baseDuration: number        // 子树最坏情况下的耗时
  startTime: number           // 渲染开始时间戳
  commitTime: number          // 提交到 DOM 的时间戳
  interactions: Set<any>      // 相关的交互追踪
}

// 性能监控回调
function onRenderCallback(
  id: string,
  phase: ProfilerPhase,
  actualDuration: number,
  baseDuration: number,
  startTime: number,
  commitTime: number,
) {
  // 记录到性能日志
  if (actualDuration > 16) {  // 超过 16ms = 掉帧阈值(60fps)
    console.warn(
      `[性能告警] ${id} 在 ${phase} 阶段耗时 ${actualDuration.toFixed(1)}ms,` +
      `超过 16ms 帧预算!`
    )

    // 上报到性能监控系统
    // reportPerformance({ id, phase, actualDuration, baseDuration })
  }

  // 开发环境输出到控制台
  if (process.env.NODE_ENV === 'development') {
    console.table({
      '组件': id,
      '阶段': phase,
      '实际耗时(ms)': actualDuration.toFixed(1),
      '基准耗时(ms)': baseDuration.toFixed(1),
    })
  }
}

// 大数据列表——潜在的性能瓶颈
function ProductList({ products }: { products: Product[] }) {
  return (
    <Profiler id="ProductList" onRender={onRenderCallback}>
      <div style={{ display: 'grid', gap: 16, gridTemplateColumns: 'repeat(3, 1fr)' }}>
        {products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </Profiler>
  )
}
逻辑代码 45 行(超过 40 行限制,仅展示)

常见的性能优化策略

TSX
// 1. React.memo — 避免不必要的重渲染
const ProductCard = React.memo(function ProductCard({
  product,
}: {
  product: Product
}) {
  return (
    <div style={{ border: '1px solid #eee', padding: 16, borderRadius: 8 }}>
      <img src={product.image} alt={product.name} width="100%" />
      <h4>{product.name}</h4>
      <p>${product.price}</p>
    </div>
  )
})

// 2. useMemo — 缓存昂贵的计算结果
function OrderSummary({ items }: { items: OrderItem[] }) {
  const totalPrice = useMemo(() => {
    return items.reduce((sum, item) => {
      // 假设这里做了复杂的汇率换算
      return sum + convertCurrency(item.price, item.currency)
    }, 0)
  }, [items])

  return <p>总计:${totalPrice.toFixed(2)}</p>
}

// 3. useCallback — 稳定的函数引用
function OrderList({ orders, onSelect }: {
  orders: Order[]
  onSelect: (id: string) => void
}) {
  // ✅ 用 useCallback 保持引用稳定
  const handleSelect = useCallback((id: string) => {
    onSelect(id)
  }, [onSelect])

  return orders.map(order => (
    <OrderRow key={order.id} order={order} onSelect={handleSelect} />
  ))
}

(4) 使用 React DevTools Components 面板调试

除了 Profiler,React DevTools 的 Components 面板也是日常调试的利器:

功能 用途 操作
组件树浏览 查看组件层级关系 点击 DevTools → Components
Props/State 实时查看 检查组件的当前状态 选中组件,查看右侧面板
直接修改 State 测试不同状态下的 UI 双击 state 值,直接编辑
搜索组件 快速定位组件 Ctrl+F 输入组件名
跳转到源代码 查看组件实现 点击 <> 图标
TEXT 📖 仅展示
// DevTools Components 面板示例
<OrderPage>
  <ErrorBoundary>
    <OrderHeader
      orderNumber="ORD-2026-0001"     ← Props 实时显示
      status="shipped"                 ← 可直接编辑测试
    />
  </ErrorBoundary>
  <ErrorBoundary>
    <OrderItems>
      <OrderItem product={...} />      ← State 展开查看
      <OrderItem product={...} />
    </OrderItems>
  </ErrorBoundary>
</OrderPage>

▶ 示例 4:API 请求错误处理——带重试的数据获取

JSX
function useFetchWithRetry(url, maxRetries = 3) {
  const [data, setData] = useState(null)
  const [error, setError] = useState(null)
  const [loading, setLoading] = useState(true)
  const [retries, setRetries] = useState(0)

  const fetchData = useCallback(async () => {
    setLoading(true)
    setError(null)
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const json = await res.json()
      setData(json)
    } catch (err) {
      if (retries < maxRetries) {
        setRetries(r => r + 1)
        setTimeout(fetchData, 1000 * (retries + 1))
      } else {
        setError(err.message)
      }
    } finally {
      setLoading(false)
    }
  }, [url, retries, maxRetries])

  useEffect(() => { fetchData() }, [url])

  return { data, error, loading, retries, refetch: () => { setRetries(0); fetchData() } }
}

function UserList() {
  const { data: users, error, loading, retries, refetch } = useFetchWithRetry('/api/users')

  if (loading) return <p>Loading... {retries > 0 && `(retry ${retries})`}</p>
  if (error) return (
    <div style={{ padding: 20, textAlign: 'center' }}>
      <p style={{ color: '#ff4d4f' }}>Error: {error}</p>
      <button onClick={refetch} style={{ padding: '8px 16px', cursor: 'pointer' }}>Retry</button>
    </div>
  )

  return (
    <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
  )
}
▶ 试一试

▶ 示例 5:全局错误监控——Sentry 集成

JSX
// lib/errorReporting.ts
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN

function initErrorReporting() {
  if (typeof window === 'undefined') return
  if (!SENTRY_DSN) return

  // Sentry.init({ dsn: SENTRY_DSN, ... })
  // 简化示例:用全局事件监听模拟

  window.addEventListener('unhandledrejection', (event) => {
    console.error('Unhandled Promise:', event.reason)
    reportError({
      type: 'unhandledrejection',
      message: event.reason?.message || String(event.reason),
      stack: event.reason?.stack,
      timestamp: new Date().toISOString(),
    })
  })

  window.addEventListener('error', (event) => {
    console.error('Global Error:', event.error)
    reportError({
      type: 'window.error',
      message: event.message,
      filename: event.filename,
      lineno: event.lineno,
      timestamp: new Date().toISOString(),
    })
  })
}

function reportError(payload) {
  fetch('/api/errors', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  }).catch(() => {})
}

// app/layout.tsx
function RootLayout({ children }) {
  useEffect(() => { initErrorReporting() }, [])
  return <html><body>{children}</body></html>
}
▶ 试一试

❓ 常见问题

Q Error Boundary 能捕获所有错误吗?
A 不能。Error Boundary 只捕获渲染阶段、生命周期方法和构造函数中的错误。以下四类错误无法捕获:事件处理中的错误(用 try/catch)、异步代码中的错误(用 Promise.catch)、服务端渲染中的错误、Error Boundary 自身的错误。设计错误防线时,需要结合 try/catch 和 Error Boundary 分层处理。
Q Profiler 火焰图中的"灰色"和"蓝色"分别代表什么?
A 灰色表示该组件在本次提交中没有重新渲染(性能好)。蓝色表示组件重新渲染了,但耗时在正常范围内。黄色或红色表示渲染耗时较长,需要优化。目标是让大多数组件在交互时保持灰色或浅蓝色。
Q React.memo 能自动优化所有组件吗?
A 不能。React.memo 只做浅比较(shallow compare)。如果 Props 中包含对象或数组,每次渲染都是新的引用,memo 会失效。此时需要配合 useMemo / useCallback 保持引用稳定,或者给 React.memo 传入第二个参数——自定义比较函数 React.memo(Comp, (prev, next) => deepEqual(prev, next))
Q <Profiler> 组件会影响生产环境性能吗?
A React 官方说明 <Profiler> 组件在生产环境会有少量性能开销。建议只在开发环境使用,或者用环境变量控制:{process.env.NODE_ENV === 'development' && <Profiler>...}。需要线上性能监控时,可以考虑专用的性能监控库(如 web-vitals)或 Sentry 的性能追踪功能。
Q Error Boundary 能用函数组件写吗?
A 目前不能。Error Boundary 依赖 getDerivedStateFromErrorcomponentDidCatch 两个生命周期,这两个只有 class 组件支持。React 团队表示未来可能提供 Hook 版本,但目前(React 18/19)只能用 class 组件。你可以写一个 class Error Boundary 组件,然后用函数组件包装它来处理错误恢复逻辑。

📖 小节


📝 作业

  1. 创建一个 ErrorBoundary 组件,在 OrderPage 中分层使用:OrderHeaderOrderItemsPaymentInfo 各用独立的 ErrorBoundary 包裹。手动制造一个渲染错误(如传入错误的 Props),验证只有出错的区域显示降级 UI,其他区域正常显示。
  2. 使用 Profiler 组件测量一个包含 100 个列表项的组件渲染耗时。用 React.memo 优化后再次测量,对比两次的 actualDuration 差异,确认优化效果。
  3. 在 Chrome 中打开 React DevTools Profiler,录制一次页面交互操作(如搜索、过滤、排序),火焰图中找出渲染耗时最长的组件,分析原因并用 useMemo / useCallback 优化。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏