项目事故错误兜底方案讨论
那一天。。。人类重新体会到了被bug捕获的恐惧!
那一天。。。不幸地直接创造了一个线上事故,呜呜呜,原因是在写某个方法的时候没有对变量正确判空,导致直接页面白屏了,一想到明天就要被组长约谈,不禁悲从中来,赶紧打开电脑想想怎么补救。
前端的项目错误一般分为几种:
- JS类的bug。
- node服务端引发的bug。
- 服务器错误引发的bug:1. 请求失败,数据渲染错误等。2. 服务器繁忙,或者直接挂了,也就是4xx / 5xx类错误。
其中:
JS类的bug是我们可以处理和捕获的,本文的主要目的就在于处理1类bug。
4xx类错误需要前端做好判空和错误处理。5xx类服务器错误会直接导致页面显示50x的问题。
我想要的应对:
- 需要增加监控,这一次我们使用的arms完全没有错误提示警告,导致错误从发版到第二天早上才被修复。
已修复,增加监控的第一点已经修复,是因为忽略了错误上报的API,现在恢复后可以查看报错信息了。 - 需要做好错误容灾处理,也就是错误捕获。
错误异常类别以及捕获方式
参考链接:
javascript 异常处理的一些经验
前端监控原理及兜底方案前端性能指标和sentry的使用
错误名 | 描述 | 示例 |
---|---|---|
EvalError | 关于 eval [1]函数的错误,已不在当前ECMAScript规范中使用,不再会被运行时抛出。 | throw new EvalError(‘EvalError’, ‘file.js’, 10); // 可以由业务代码主动抛出 |
RangeError | 值不在允许的范围内,典型的是试图传递一个数值给一个范围内不包含该数值的函数,此时应该引发RangeError。 | const numObj = 123;numObj.toFixed(-1); // Uncaught RangeError: toFixed() digits argument must be between 0 and 100 at Number.toFixed |
ReferenceError | 当一个不存在(或尚未初始化)的变量被引用时发生的错误。 | const a = undefinedVariable; // Uncaught ReferenceError: undefinedVariable is not defined |
SyntaxError | 解析代码阶段,发现了不符合语法规范的代码。 | const 111variable = 1; // Uncaught SyntaxError: Invalid or unexpected token |
TypeError | 类型错误,用来表示值的类型是非预期类型。 | const a = null;a.doSomeThing(); // Uncaught TypeError: Cannot read properties of null (reading ‘doSomeThing’) |
URIError | 使用URI处理函数产生的错误 | decodeURIComponent(‘%’) // Uncaught URIError: URI malformed |
其他自定义Error | 自定义Error可以命名更加清晰 |
class AccountException extends Error {
constructor(message) {
super(`AccountException: ${message}`);
}
}
const AccountController = {
getAccount: (id) => {
...
throw new RequestException('请求账户信息失败!');
...
}
}
客户端错误捕获
同步JS任务中的bug可以用try…catch
只有在执行过程中的异常可以被捕获,语法解析阶段的异常或者不在当前同步任务中的异常都无法被捕获。
全局bug捕获
全局中可以使用:
window.onerror
和window.addEventListener('error')
去做最后兜底捕获同步错误。
window.addEventListener('unhandledrejection')
捕获异步、promise错误。
在大多数情况下addEventListener(‘error’)和window.onerror的效果差不多。在浏览器中有两种事件机制,捕获和冒泡,这两个方法就分别是通过捕获和冒泡来拿到error的。
但是对于资源的加载错误事件中,canBubble: false,所以理所应当的window.onerror是拿不到资源加载错误的,而addEventListener则可以拿到错误。但是在拿到错误以后需要简单的区分一下是资源加载错误还是其他错误,因为该方法也能够捕获语法错误等一系列其他错误。
方法也很简单,他们之间有一个很明显的区别,其他的普通错误会有一个message字段,资源加载错误没有这个字段,这样只要让这一段代码运行在所有资源之前,那就可以拿到这方面的错误了。
window.addEventListener('error', (errorEvent) => {
console.log(errorEvent)
cosnole.log(errorEvent.message)
}, true)
服务端错误捕获
跟客户端差不多,也分同步、异步错误分类。
uncaughtException
通过Node的全局处理,捕获所有未被处理的错误,这是最后一层关卡,兜底的操作,如果还不处理的话往往会导致程序崩溃。
process.on('uncaughtException', err => {
//do something
});
unhandledRejection
在Node中,Promise中的错误同样不能被try...catch
和uncaughtException
捕获。这时候我们就需要unhandledRejection来帮我们捕获这部分错误。
process.on('unhandledRejection', err => {
//do something
});
借助框架对异常的处理(以koa为例)
对于Node端我们往往,可以借助框架对错误进行捕获,像koa就可以通过app.on error对错误在框架这一层进行捕获,同样他也是捕获内部没有被catch到的错误,像promise错误并不能捕获。
app.on('error', (err, ctx) => {
// do something
});
前端白屏问题
参考:
前端白屏监控探索
React捕获错误getDerivedStateFromError
React错误边界
介绍了这么多错误类型和捕获,我的项目也修改得七七八八了,但是仍然只是错误发生之后才能得知,我当时发生的问题在于一个JS错误直接导致白屏,所以我在想如何才能让错误直接不暴露出来呢?局部UI组件直接导致了全局的失控,代价有点大。
只有在render中需要展示在页面上的bug,才会导致组件崩溃的白屏错误,如果只是在函数中的运行bug,会抛出错误但是不会导致页面崩溃。
根据参考,可以使用getDerivedStateFromError
生命周期来捕获子级组件的白屏错误,使本该白屏的组件实现UI上的优雅降级,展示错误提示,不至于直接显示出全白屏。
注意的是只有class组件可以使用这个生命周期捕获错误边界,而且因为错误是逐级抛出,错误处理组件必须放在父级外层包裹才能捕获。
class ErrorBoundary extends 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>
<Component />
</ErrorBoundary>