React 中错误边界无法捕获异步错误
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
Promise中reject或throw但未提供.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.onerror 和 window.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>
);
}