React 中错误边界无法捕获异步错误

FreeGuideOnline 最新 2026-07-07

jsx class ErrorBoundary extends React.Component { state = { hasError: false };

static getDerivedStateFromError(error) { return { hasError: true }; }

componentDidCatch(error, errorInfo) { logErrorToService(error, errorInfo); }

render() { if (this.state.hasError) return

出错了!

; return this.props.children; } }


只在子组件树的**渲染阶段**、**生命周期方法**和**构造函数**中抛出的错误会被捕获。

## 异步错误与传统错误的区别

传统同步错误发生时,JavaScript 引擎会沿着调用栈逐层抛出,直到被最近的 `catch` 块或错误边界捕获。而异步错误(例如 `setTimeout`、`Promise`、`async/await`)发生时,**其执行上下文已经脱离原始调用栈**,错误无法按同步方式向上传播。

```jsx
// 同步错误:可被错误边界捕获
class SyncComponent extends React.Component {
  render() {
    throw new Error('同步错误');
  }
}

// 异步错误:无法被错误边界捕获
class AsyncComponent extends React.Component {
  componentDidMount() {
    setTimeout(() => {
      throw new Error('异步错误'); // 错误边界捕获不到
    }, 1000);
  }
  render() { return <div>正常内容</div>; }
}

为什么错误边界不能捕获异步错误?

事件循环与调用栈

JavaScript 采用单线程事件循环机制。当 setTimeout 的回调在未来某个时刻执行时,其调用栈已经不再包含 React 组件树的上下文。错误抛出时,React 无法将其与特定的组件树关联,因此错误边界无法介入。

React 的错误捕获机制

React 通过包裹组件的生命周期函数和渲染函数来捕获错误。像 componentDidMount 这样的生命周期是同步执行的,但其中启动的异步操作(如 setTimeout、网络请求)并不在 React 的管控范围内。React 没有办法追踪这些异步回调的执行流。

常见异步错误场景

所有在 React 控制范围之外执行的代码抛出的错误,都不会被错误边界感知。常见场景包括:

  • setTimeout / setInterval
    回调函数中抛出的错误。
  • Promise 未处理的 rejection
    Promiserejectthrow 但未提供 .catch() 处理。
  • async/await 中的异常
    async 函数内部错误本质上是一个 rejected promise。
  • 事件处理器中的异步操作
    例如 onClick 中发起的异步请求,其错误链无法被包装的错误边界捕获。
  • 第三方库或非 React 代码
    直接在 DOM 上绑定的事件或使用第三方异步工具。

解决方案

方案一:在异步操作内部 try-catch 并更新状态

最直接的方式是在异步回调中捕获错误,然后通过状态变更来展示回退 UI。

function MyComponent() {
  const [error, setError] = useState(null);

  const handleClick = async () => {
    try {
      await fetchData();
    } catch (err) {
      setError(err); // 自行管理错误状态
    }
  };

  if (error) return <div>加载失败</div>;
  return <button onClick={handleClick}>获取数据</button>;
}

这种方式本质上把错误处理从声明式的错误边界搬到了组件内部,适用于局部 UI。

方案二:全局错误处理

利用 window.onerrorwindow.addEventListener('unhandledrejection') 捕获未被处理的异步错误,并统一展示错误提示或记录日志。

// 在应用入口设置
window.addEventListener('error', (event) => {
  console.error('捕获到全局错误:', event.error);
  // 可以更新顶层状态或展示全局错误对话框
});

window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise 拒绝:', event.reason);
});

这一方案的弱点是无法精确到具体的组件树,只能作为最后的兜底手段。

方案三:使用高阶组件包装异步逻辑

自定义高阶组件(HOC)或容器组件,为异步操作提供一个安全的执行环境。

function withAsyncBoundary(WrappedComponent) {
  return class extends React.Component {
    state = { error: null };

    componentDidCatch(error) { /* 仍无法捕获异步错误 */ }

    // 提供一个 helper 方法让子组件安全地执行异步操作
    safeAsync = async (fn) => {
      try {
        await fn();
      } catch (err) {
        this.setState({ error: err });
      }
    };

    render() {
      if (this.state.error) return <div>操作失败</div>;
      return <WrappedComponent safeAsync={this.safeAsync} {...this.props} />;
    }
  };
}

子组件使用注入的 safeAsync 包裹所有异步调用,即可将错误交给父级处理。

方案四:使用 react-error-boundary 处理异步错误

社区库 react-error-boundary 提供了更灵活的 useErrorBoundary 钩子,并结合 ErrorBoundary 组件,可以手动“抛出”错误让最近的边界捕获。

import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

function MyComponent() {
  const { showBoundary } = useErrorBoundary();

  const loadData = async () => {
    try {
      await fetchSomeData();
    } catch (err) {
      showBoundary(err); // 手动将错误传递给最近的 ErrorBoundary
    }
  };

  return <button onClick={loadData}>加载</button>;
}

// 使用
<ErrorBoundary FallbackComponent={ErrorFallback}>
  <MyComponent />
</ErrorBoundary>

这样,异步错误也能被错误边界优雅地捕获并展示统一的降级 UI。

实践示例

示例1:setTimeout 中的错误捕获

存在一个组件延迟执行一段代码,但可能出错:

class BadComponent extends React.Component {
  componentDidMount() {
    setTimeout(() => {
      throw new Error('延迟错误');
    }, 1000);
  }
  render() { return '内容'; }
}

改进:在 setTimeout 内部使用 try-catch,并调用父组件传入的错误处理函数。

function GoodComponent({ onError }) {
  useEffect(() => {
    const timer = setTimeout(() => {
      try {
        riskyOperation();
      } catch (err) {
        onError(err);
      }
    }, 1000);
    return () => clearTimeout(timer);
  }, [onError]);
  return '内容';
}

// 父组件
function Parent() {
  const [error, setError] = useState(null);
  return error ? <div>出错了</div> : <GoodComponent onError={setError} />;
}

示例2:使用 react-error-boundary 处理异步错误

安装库:npm install react-error-boundary

import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>发生错误{error.message}</p>
      <button onClick={resetErrorBoundary}>重试</button>
    </div>
  );
}

function AsyncDataComponent() {
  const { showBoundary } = useErrorBoundary();

  const fetchData = async () => {
    try {
      const res = await fetch('/api/data');
      if (!res.ok) throw new Error('请求失败');
      return res.json();
    } catch (err) {
      showBoundary(err);
    }
  };

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

  return <div>数据已加载</div>;
}

export default function App() {
  return (
    <ErrorBoundary FallbackComponent={Fallback}>
      <AsyncDataComponent />
    </ErrorBoundary>
  );
}