引言
过去,组件内出现JavaScript异常时会导致React内部的state被破坏并且在下一次渲染时抛出 可能无法跟踪的 异常。这些错误基本上都是有早期代码(非React组件代码)造成的,但是React并没有提供能够优雅地在组件中处理和回复这些异常的方法。
异常捕获边界(Error Boundaries)
在部分UI中出现的JavaScript异常是不应该导致整个应用的崩溃的。为了解决这个问题,React16引进了一个新的概念“异常捕获边界(Error Boundaries)
“。
异常捕获边界是一种React组件,它能够捕获在它子组件树中出现的任何JavaScript异常,将它们打印出来并展示一个备用UI,这样就不会导致组件树的崩溃。异常捕获边界能够捕获它的子组件数中在渲染,生命周期方法和构造函数中出现的任何异常。
注意: 异常捕获边界不会捕获下列异常:
- 事件处理(了解更多)
- 异步代码(如
setTimeout
或requestAnimationFrame
回调函数)- 服务端渲染
- 异常捕获边界自身抛出的异常(不是它的子元素抛出的异常)
只要在组件中定义其中一个及以上的生命周期方法(static getDerivedStateFromError()或 componentDidCatch()),那么这个组件就变成了异常捕获边界。当异常被抛出时使用static getDerivedStateFromError()
来渲染一个备用UI,使用componentDidCatch()
来打印异常信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
//更新state后再下次渲染时会显示备用UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
//你也可以在异常报告设备中打印异常信息
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
//你可以渲染自定义的任意备用UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
现在你就可以像正常组件一样使用它了:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
异常捕获边界的工作机制类似于JavaScript的catch{}
模块,但是它是捕获组件抛出的异常的。只有class组件能够成为异常捕获边界。在实践中,绝大部分实践我们只想要声明一个异常捕获边界,然后再整个应用中使用它。
注意异常捕获边界只会捕获那些作为它的子组件的组件抛出的异常。但它不会捕获自身抛出的异常。如果一个异常捕获边界未能成功渲染异常信息,那么它会把这个异常信息传递给离他最近的祖先异常捕获边界组件。这也是类似于JavaScript catch{}
模块的工作机制。
异常捕获边界的放置位置
异常捕获边界的粒度由你来决定。你可以包裹顶层组件来向用户展示“有什么地方出错了”,就像服务端框架处理崩溃一样。你也可以将小部件包裹再异常处理边界中来防止它崩溃时影响其他部分。
未捕获异常的新行为
这一改变有着重要的意义。在React16中,任何没有被异常捕获边界捕获的异常将会导致整个React组件树的卸载。
对这个决定我们也存在着争议,但是在我们以往的经验中,不处理崩溃的UI比将它们完全移除更加糟糕。比如在社交软件这类产品中,崩溃的UI留在界面上可能会导致用户将信息发送给错误的对象。同样的,在支付软件中显示错误的金额比什么也不展示造成的影响更大。
这一改变意味着当你迁移到React16时,你可能会发现之前没有发现的错误。使用异常捕获边界能够让你的应用在出现异常时提供一个更好的用户体验。
比如,Facebook Messager将侧边栏、信息面板、聊天记录以及信息输入框包裹在单独的异常捕获边界中,这样如果其中的某一部分崩溃了,也不影响其他部分的正常运行。
同时我们也推荐使用JS错误报告服务(或者自行构建),这样就可以了解在生产环境中未捕获的异常信息并且修复它们。
组件栈追踪
在开发环境中,React16会将渲染过程中出现的所有异常都打印到控制台上,即使应用以外地将它们掩盖了。除了异常信息和JavaScript栈,React16同时还提供了组件栈追踪功能。现在你可以看到异常发生在组件树中的具体位置了。

你也可以在组件栈追踪中看到异常所在的文件名和行数。这在通过Create React App创建的项目中默认执行。

如果你没有使用Create React App,你可以在Babel配置中手动添加这个插件。注意这只是在开发环境中使用的,在生产环境中一定要关闭这个功能。
注意: 在栈追踪中展示的组件名称取决于Function.name属性。如果你想要支持尚未提供该功能的浏览器或设备(比如IE11),考虑在你的打包应用程序中加入一个包含
Function.name
的polyfill
,比如function.name-polyfill。作为替代的,你也可以在你的组件中显式地设置displayName属性。
关于try/catch?
try/catch很棒但它只能用于命令式代码(imperative code):
try {
showButton();
} catch (error) {
// ...
}
但是React组件是声明式的并且明确指出了什么是要被渲染的:
<Button />
异常捕获边界保留了React声明式的特性并且能够像你预期的一样工作。即使在组件树的深层层级中的componentDidUpdate
方法中调用setState
方法发生了异常,它也会将这个异常传递给最近的那个异常捕获边界。
关于事件处理器
异常捕获边界不会捕获发生在事件处理器中的异常。
React不需要通过异常捕获边界来修复发生在事件处理器中的异常。不想render方法和生命周期方法,事件处理器不会再渲染期间被调用。所以即使事件处理器抛出了异常,React任然知道需要展示什么。
如果你需要在事件处理器中捕获异常,请使用常规的JavaScript try/catch语句:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
try {
// 会抛出异常的操作
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
return <h1>Caught an error.</h1>
}
return <div onClick={this.handleClick}>Click Me</div>
}
}
注意上面的例子只是展示了常规的JavaScript行为,并没有使用异常捕获边界。
自React15的命名更改
React15有一个对异常捕获支持有限的方法:unstable_handleError
。这个方法现在已经不再起作用了,自使用React16开始,你需要把unstable_handleError
方法改为componentDidCatch
。
为了这个改变,我们提供了codemod来自动迁移你的代码。