作者:王特
前言
异常与错误是我们开发,甚至线上难免会遇到的问题,触发的原因可能是各种各样的。但是即使运行时页面中偶现错误,我们也需要保证页面的稳定性,并且因此错误的捕获尤为重要,此外,线上监控能够发现问题、排查问题的前提也是代码中的错误被正确的捕获并且上报。本文尽可能详细的介绍了可能存在的错误与捕获方式。
前端错误分类
| 错误分类 | 解决方案 | 错误示例 |
|---|---|---|
| 运行错误 | 错误捕获(下面详细探讨) | |
| 资源加载错误 | 错误捕获window.addEventListener('error', event => {}); | |
| Ajax 请求错误 | 重写错误处理 | 运行错误 + http 错误 + 自定义错误 |
| 网页崩溃 | Service worker线程心跳 | -- |
我们重点探讨下运行错误
运行错误的影响
function foo() {
console.log('foo function work 1');
setTimeout(() => {
console.log('some async work');
});
throw new Error('setTimeout error');
console.log('foo function work 2');
console.log('foo function work 3');
console.log('foo function work 4');
}
console.log('global work 1');
foo();
console.log('global work 2');
console.log('global work 3');
运行结果:
影响:
运行错误,只影响同步代码的执行,异步代码无影响。try catch的作用可以进一步缩小错误影响范围。
错误捕获方式
我们从几个例子逐步来探讨
异步任务错误捕获
代码片段一
try {
setTimeout(() => {
throw new Error('setTimeout error');
});
throw new Error('try block error');
} catch (error) {
console.log('Caught', error);
}
运行结果
相关拓展
try {
window.requestAnimationFrame(() => throw new Error('some error'))
} catch(error) {
console.log(error);
}
结论
- 异步任务无法使用try catch捕获。
捕获兜底
// window 全局检测错误,非捕获
window.onerror = (error) => {
console.log('Window Onerror:', error);
}
window.addEvenListener('error', e => {}, true)
Promise 错误捕获
Promise 语法结构中,有同步执行的部分,也有异步执行的部分,接下来我们来研究下这两部分的执行效果。
代码片段一
try {
new Promise((resolve, reject) => {
throw new Error('promise error');
});
throw new Error('try block error');
} catch (error) {
console.log('Caught', error);
}
运行结果
相关拓展
try {
Promise.resolve().then(() => {
thw new Error omise th rror');
})
} catch(error) {
console.log('Caught', error);
}
结论
- Promise 内部有自建的错误处理机制,Promise相关的同步和异步代码错误,都走自建的错误处理。
捕获兜底
window.addEventListener('unhandledrejection', event => {
/* 你可以在这里添加一些代码,以便检查
event.promise 中的 promise 和
event.reason 中的 rejection 原因 */
console.log('unhandledrejection', event);
event.preventDefault();
}, false);
window.addEventListener("rejectionhandled", event => {
console.log("Promise rejected; reason: " + event.reason);
}, false);
Generator 错误捕获
代码片段一
function* generator() {
console.log('generator begin');
yield 1;
throw new Error('generator error');
console.log('generator second step');
yield 2;
console.log('generator third step');
}
(function(gen) {
try {
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
} catch (error) {
console.log('Caught', error);
}
})(generator());
运行结果:
代码片段二
function* generator() {
try {
console.log('generator begin');
yield 1;
throw new Error('generator error');
console.log('generator second step');
yield 2;
console.log('generator third step');
} catch (error) {
console.log('Caught Error:', error);
}
}
(function(gen) {
for (let i = 0; i < 3; i++) {
setTimeoout(() => gen.next(), i * 1000)
}
})(generator());
Async 错误捕获
代码片段一
async function asyncFun() {
throw new Error('async error');
}
(async function () {
try {
await asyncFun();
throw new Error('try block error');
} catch (error) {
console.log('Caught', error);
}
})();
运行结果
相关拓展
async function asyncFun() {
throw new Error('async error');
}
(async function () {
try {
asyncFun();
throw new Error('try block error');
} catch (error) {
console.log('Caught', error);
}
})();
结论
- Async await 是基于Promise、Generator的语法糖,其错误处理遵循Promise的处理方式;
- await 有将Promise错误转成同步错误的作用。
React 错误捕获
代码片段一
function ChildNode() {
throw new Error('ChildNode render error');
return <span>This is a child node.</span>
}
function ParentNode() {
try {
return <div><ChildNode/></div>;
} catch (error) {
return <div>Render Error</div>;
}
}
代码片段二
function ChildNode() {
throw new Error('ChildNode render error');
return <span>This is a child node.</span>
}
class ParentNode extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>Render Error</div>;
}
return <div><ChildNode/></div>;
}
}
探索原理
- React 的错误捕获基于 Fiber 树,理解了 Fiber,就很容易理解 React 的错误捕获。错误捕获范围仅限于 Fiber 的执行内容,不包含以下内容:
源码路径:packages/react-reconciler/src/ReactFiberThrow.new.js
- React 错误边界,是检测,而非捕获。即使有边界,错误同样也会被抛出,参考下图官方示例:
总结
运行错误类型
| 错误类型 | 简介 | 错误示例 |
|---|---|---|
| EvalError | eval 函数中发生的错误 | 根据EcmaSpec 2018版: 此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性。 |
| InternalError | 内部错误 | 该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。示例场景通常为某些成分过大,例如:“too many switch cases”(过多case子句);“too many parentheses in regular expression”(正则表达式中括号过多);“array initializer too large”(数组初始化器过大); |
| RangeError | 数值范围错误 | |
| ReferenceError | 引用错误 | |
| SyntaxError | 语法错误 | |
| TypeError | 类型错误 | |
| URIError | 全局URI处理函数错误 |
运行错误处理分类总结
| 分类总结 | 涉及错误 | 解决方案 |
|---|---|---|
| 同步错误 | 同步错误 | try catchwindow.onerrorwindow.addEvenListener('error', e => {}, true) |
| 异步错误 | 除Promise外的其他异步错误 | window.onerrorwindow.addEvenListener('error', e => {}, true) |
| Promise | window.addEvenListener('unhandledrejection', e => {}, true)window.addEvenListener('rejectionhandled', e => {}, true) |
运行错误处理方式总结
| 错误捕获方式 | 特点 | ||||
| try catch | - 局部 | ||||
| window.onerror | - 全局
| ||||
| window.addEvenListener('error', e => {}, true) | - 全局
| ||||
| window.addEvenListener('unhandledrejection', e => {}, true) | - 全局
| ||||
| window.addEvenListener('rejectionhandled', e => {}, true) | - 全局
| ||||
参考文档
es6.ruanyifeng.com/?from=from_…