javascript 异常处理的一些经验
写在前面
为了提升应用稳定性,我们对前端项目开展了脚本异常治理的工作,对生产上报的js error进行了整体排查,试图通过降低脚本异常的发生频次来提升相关告警的准确率,结合最近在这方面阅读的相关资料,尝试阶段性的做个总结,下面我们来介绍下js异常处理的一些经验。
先说概念
描述的很简单,我们总结一下就是代码在 执行过程中 遇到了问题,程序已经无法正常运行了,Error对象会被抛出,这一点它不同于大部分编程语言里使用的异常对象Exception,甚至更适合称之为错误,应该说事实也确实如此,Error对象在未被抛出时候和js里其他的普通对象没有任何差别是不会引发异常的,同时Error 对象也可用于用户自定义错误的基础对象。 看下面两个例子:Error objects are thrown when runtime errors occur. The Error object can also be used as a base object for user-defined exceptions.
↓↓↓执行结果↓↓↓try {const 123variable = 2;} catch(e) {console.log('捕获到了:', e)}
↓↓↓执行结果↓↓↓<script>function throwSomeError() {throw new Error('抛个异常玩玩');console.log('我估计是凉了,不会执行我了!');}throwSomeError();console.log('那么我呢?')</script><script>console.log('大家猜猜我会执行吗?');</script>
| 错误名 | 描述 | 示例 |
| EvalError |
|
|
| RangeError | 值不在允许的范围内,典型的是试图传递一个数值给一个范围内不包含该数值的函数,此时应该引发RangeError。 |
const numObj = 123;
|
| ReferenceError | 当一个不存在(或尚未初始化)的变量被引用时发生的错误。 |
|
| SyntaxError | 解析代码阶段,发现了不符合语法规范的代码。 |
|
| TypeError | 类型错误,用来表示值的类型是非预期类型。 |
const a = null;
|
| URIError | 使用URI处理函数产生的错误 |
|
// 获取分页数据const getPagedData = (pageIndex, pageSize) => {if(pageIndex < 0 || pageSize < 0 || pageSize > 1000) {throw new RangeError(`pageIndex 必须大于0, pageSize必须在0和1000之间`);}return [];}// 转换时间格式const dateFormat = (dateObj) => {if(dateObj instanceof Date) {return 'formated date string';}throw new TypeError('传入的日期类型错误');}
3、技术上来讲,你可以抛出任何类型的异常,而不仅仅是Error的实例,但请不要这么做,总是抛出正确的错误对象会让我们更容易定位问题,同时可以保持错误处理的一致性,捕获异常时候也总能够拿到Error实例上的message和stack;new Error('出错了!');console.log('我吃嘛嘛香,喝嘛嘛棒!'); // 正常输出 '我吃嘛嘛香,喝嘛嘛棒!'
// badthrow '出错了';throw 123;throw [];throw null;
被忽略的finally:此语句块会在try和catch语句结束之后执行,无论结果是否报错。 同时要注意,异步中的发生的异常无法被上层捕获,比如:try {// 要运行的代码,可能引发异常doSomethingMightThrowError();}catch (error) {// 处理异常的代码块,当发生异常时将会被捕获,如果不继续throw则不会再向上传播// error为捕获的异常对象// 这里一般能让程序恢复的代码doRecovery();}finally {// 无论是否出现异常,始终都会执行的代码doFinally();}
Promise本身是就可以捕获异常,语法上也类似于try catch,一旦发生异常,程序跳过promise内的代码继续执行;可以使用了catch方法捕获后进行处理,也可以使用then方法中的第二个参数处理异常。promise的异常对象同样是冒泡的,前者捕获了就不会抛给后者,参见示例:// Timeouttry {setTimeout(() => {throw Error("定时器出错了!");}, 1000);} catch (error) {console.error(error.message);}// Eventstry {window.addEventListener("click", function() {throw Error("点击事件出错了!");});} catch (error) {console.error(error.message);}
const promiseA = new Promise((resolve,reject)=>{throw new Error('Promise出错了!');});const doSomethingWhenResolve = () => {};const doSomethingWhenReject = (error) => {logger.log(error)}// 使用catch捕获const promiseB = promiseA.then(doSomethingWhenResolve).catch(doSomethingWhenReject);// 等价于const promiseB = promise.then(doSomethingWhenResolve, doSomethingWhenResolve);promiseB.then(() => {console.log('我又可以正常进到then方法了!');}).catch(()=>{console.log('不会来这里!');})
如何处理异常
异常的发生不可避免,所以在软件开发中,合理的异常处理就成为了高质量代码不可或缺的一部分,只有处理好了异常我们才能对程序中的意外情况进行有效的控制。我们最容易容易犯的一个问题就是将异常处理和业务的流程混为一谈。 根据Clean Code的建议,面对异常我们可以遵循以下一些原则,提高代码质量:
// Dirtyclass Laptop {sendShutDown() {const deviceID = getID(DEVICE_LAPTOP);if (deviceID !== DEVICE_STATUS.INVALID) {pauseDevice(deviceID);clearDeviceWorkQueue(deviceID);closeDevice(deviceID);} else {logger.log('Invalid handle for: ' + DEVICE_LAPTOP.toString());}}getID(status) {...// 总是会返回deviceID,无论是不是合法有效的return deviceID;}}// Cleanclass Laptop {sendShutDown() {try {tryToShutDown();} catch (error) {logger.log(error);}}tryToShutDown() {const deviceID = getID(DEVICE_LAPTOP);pauseDevice(deviceID);clearDeviceWorkQueue(deviceID);closeDevice(deviceID);}getID(status) {...throw new DeviceShutDownError('Invalid handle for: ' + deviceID.toString());...return deviceID;}}
// badtry {doSomethingMightThrowError();} catch (error) {console.log(error);}// goodtry {doSomethingMightThrowError();} catch (error){console.error(error);message.error(error.message);logger.log(error);}
// badfetchData().then(doSomethingMightThrowError).catch(console.log);// goodfetchData().then(doSomethingMightThrowError).catch(error => {console.error(error);message.error(error.message);logger.log(error);});
export class RequestException extends Error {constructor(message) {super(`RequestException: ${mesage}`);}}export class AccountException extends Error {constructor(message) {super(`AccountException: ${message}`);}}const AccountController = {getAccount: (id) => {...throw new RequestException('请求账户信息失败!');...}}// 客户端代码,创建账户const id = 1;const account = AccountController.getAccount(id);if(account){throw new AccountException('账户已存在!');}
React中的建议
局部UI的JS Error不应该导致整个应用崩溃白屏,我们应该把他的影响范围控制在最小,这是一个容易形成共识的结论,于是React 16引入了错误边界(Error Boundaries)的概念。
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。
所以给我们的启示是组件库或者业务系统中的块级的一些东西(spm模型中的c位)一定要考虑好组件级别的异常处理。import { Result } from 'antd';import type { ErrorInfo } from 'react';import React from 'react';// eslint-disable-next-line @typescript-eslint/ban-typesclass ErrorBoundary extends React.Component<{ children?: React.ReactNode },{ hasError: boolean; errorInfo: string }> {state = { hasError: false, errorInfo: '' };static getDerivedStateFromError(error: Error) {return { hasError: true, errorInfo: error.message };}componentDidCatch(error: any, errorInfo: ErrorInfo) {// You can also log the error to an error reporting service// eslint-disable-next-line no-consoleconsole.log(error, errorInfo);}render() {if (this.state.hasError) {// You can render any custom fallback UIreturn <Result status="error" title="Something went wrong." extra={this.state.errorInfo} />;}return this.props.children;}}export { ErrorBoundary };
异常的全局上报
基本上这是对付不可预知异常的终极解法,自动收集错误报告并在达到阈值时做出告警,属于在理想情况下异常发生后能让研发同学们能第一时间发现并定位解决问题,主要会使用2个全局事件:
// message:错误信息(字符串)。// source:发生错误的脚本URL(字符串)// lineno:发生错误的行号(数字)// colno:发生错误的列号(数字)// error:Error对象(对象)window.onerror = function(message, source, lineno, colno, error) {logger.log('捕获到异常:',{ message, source, lineno, colno, error });}
window.addEventListener('unhandledrejection', (e) => {console.error('catch', e)}, true)
写在最后
其实总结下来我们的异常处理主要也只是干两件事情: 1、将面向开发的异常信息转换成更友好的用户界面提示; 2、将异常信息上报到服务端让研发同学去解决这些异常; 希望大家看了本篇文章有所收获! 参考链接:
[1]https://developer.mozilla.org/zh-CN/Core_JavaScript_1.5_Reference/Global_Functions/eval
[2]https://reactjs.org/docs/error-boundaries.html [3]https://procomponents.ant.design/
往期推荐
1. 聊一聊分布式锁的设计模型
工匠追求“术”到极致,其实就是在寻“道”,且离悟“道”也就不远了,亦或是已经得道,这就是“工匠精神”——一种追求“以术得道”的精神。 如果一个工匠只满足于“术”,不能追求“术”到极致去悟“道”,那只是一个靠“术”养家糊口的工匠而已。作者根据多年来的实践探索,总结了大量的Java编码之“术”,试图阐述出心中的Java编码之“道”。
点击阅读原文查看详情。