javascript 异常处理的一些经验

发表于 2年以前  | 总阅读数:474 次

写在前面

为了提升应用稳定性,我们对前端项目开展了脚本异常治理的工作,对生产上报的js error进行了整体排查,试图通过降低脚本异常的发生频次来提升相关告警的准确率,结合最近在这方面阅读的相关资料,尝试阶段性的做个总结,下面我们来介绍下js异常处理的一些经验。 先说概念

什么是异常

先来看一下官方的定义:

Error objects are thrown when runtime errors occur. The Error object can also be used as a base object for user-defined exceptions.

描述的很简单,我们总结一下就是代码在执行过程中遇到了问题,程序已经无法正常运行了,Error对象会被抛出,这一点它不同于大部分编程语言里使用的异常对象Exception,甚至更适合称之为错误,应该说事实也确实如此,Error对象在未被抛出时候和js里其他的普通对象没有任何差别是不会引发异常的,同时Error 对象也可用于用户自定义错误的基础对象。

看下面两个例子:


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>

↓↓↓执行结果↓↓↓

以上红色信息里包含了异常信息(message)和栈跟踪(stack trace)信息,对于定位代码中的问题起到重要作用,可以看到栈跟踪是从底部文件位置21:15到顶部25:7位置的;前两个console在遇到异常时候未被执行,第二个script标签内的代码被正常执行。

结论:当任务执行过程中出现未处理的异常,会一直沿着调用栈一层层向外抛出(有点像事件冒泡),最终会导致当前任务被终止执行。当前任务终止后JS 线程会继续从任务队列中提取下一个任务继续执行。

异常的类型

错误名 描述 示例
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

1、以上这些异常很多都来会由Javascript引擎抛出,但异常类型都是实际的构造函数,旨在生成一个新的异常实例,所以你可以:


// 获取分页数据
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('传入的日期类型错误');
}

2、Error实例被创建时不能被称之为异常,只有在使用throw关键字将其抛出时才会引发异常;


new Error('出错了!');
console.log('我吃嘛嘛香,喝嘛嘛棒!'); // 正常输出 '我吃嘛嘛香,喝嘛嘛棒!'

3、技术上来讲,你可以抛出任何类型的异常,而不仅仅是Error的实例,但请不要这么做,总是抛出正确的错误对象会让我们更容易定位问题,同时可以保持错误处理的一致性,捕获异常时候也总能够拿到Error实例上的message和stack;


// bad
throw '出错了';
throw 123;
throw [];
throw null;

异常捕获

前面有提到如果引发异常后不做任何处理会冒泡似的在你的调用栈中向顶部传播,直到导致当前任务崩溃。有时候发生致命错误时候我们确实希望安全的停止程序的运行,如果希望程序得以恢复一般我们会用到try...catch...finally代码结构,它是js中处理异常的标准方式;


try {
  // 要运行的代码,可能引发异常
  doSomethingMightThrowError();
} 
catch (error) {
  // 处理异常的代码块,当发生异常时将会被捕获,如果不继续throw则不会再向上传播
  // error为捕获的异常对象
  // 这里一般能让程序恢复的代码
  doRecovery();
}
finally {
  // 无论是否出现异常,始终都会执行的代码
  doFinally();
}

被忽略的finally:此语句块会在try和catch语句结束之后执行,无论结果是否报错。

同时要注意,异步中的发生的异常无法被上层捕获,比如:


// Timeout
try {
  setTimeout(() => {
    throw Error("定时器出错了!");
  }, 1000);
} catch (error) {
  console.error(error.message);
}

// Events
try {
  window.addEventListener("click", function() {
    throw Error("点击事件出错了!");
  });
} catch (error) {
  console.error(error.message);
}

Promise本身是就可以捕获异常,语法上也类似于try catch,一旦发生异常,程序跳过promise内的代码继续执行;可以使用了catch方法捕获后进行处理,也可以使用then方法中的第二个参数处理异常。promise的异常对象同样是冒泡的,前者捕获了就不会抛给后者,参见示例:


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的建议,面对异常我们可以遵循以下一些原则,提高代码质量:

Prefer Exceptions to Returning Error Codes

优先选择异常而不是错误码。

要理解这句话还是得结合例子,下面的第一段代码定义了一个Laptop类,在它的sendShutDown方法实现中,用if语句去检查了getID的返回值中是否存在无效的deviceID,错误检查会使调用者的代码变得复杂不易阅读业务逻辑,同时如果这个错误检查被遗漏也会导致代码出现问题,这个错误的处理可以交给语言让整个过程更加优雅。第二段代码中则将异常处理隔离了两个不同的逻辑,这样做会带来一些优势:

1、业务流程更加清晰易读,我们把异常和业务流程理解为两个不同的问题,可以分开去处理;

2、分开来的两个逻辑都更加聚焦,代码更简洁;

3、将处理程序异常的职责交给了编程语言,明确了边界;


// Dirty
class 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;
  }
}
// Clean
class 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;
  }
}

Don't ignore caught error!

捕获到异常后不要忽略异常处理!

在之前的代码评审中就经常有看到我们同学会在catch块中什么都不做,或者迫于eslint的检查会写一个console.log(error),这同样意味着什么都没有做。属于眼睁睁看到异常发生了不采取任何措施,这样的处理方式非常危险,因为这些异常通常由我们没有考虑到的意外情况引起,从中能发现业务逻辑中不易发现的问题,一旦我们捕获了这些异常,顶层的错误监控也不能主动捕获到这些问题,程序也许没有崩溃但如果没有用户告知我们,我们就无法发现用户的哪些功能无法正常使用了,因此最起码也要对这些异常做日志上报;


// bad
try {
  doSomethingMightThrowError();
} catch (error) {
  console.log(error);
}

// good
try {
  doSomethingMightThrowError();
} catch (error){
  console.error(error);
  message.error(error.message);
  logger.log(error);
}

Don't ignore rejected promises!

不要轻易忽略Promise的异常,除非你确定它已经被处理了!

这一块我们还是有血泪教训的,在接入AEM的项目中曾经在脚本异常的上报里将disable_unhandled_rejection开启,禁止捕获了所有Promise异常,当时是基于我们线上应用大部分的promise异常都是umi-request请求接口出错和antd表单验证错误,且未带来什么线上问题,于是就天真的认为未捕获的promise异常毫无危害;这个想法同样危险,因为深入跟踪发现接口请求出错请求库捕获了异常并使用了message.error进行处理,表单验证错误的异常同样是antd在处理完之后选择继续向上抛出,这两者确实没什么危害,可当我们面对这些更多未做处理的Promise异常时候(比如接口返回成功但约定的数据格式错误)同时又不做上报,我们就损失了很多线上问题的案发现场,只能抓瞎去盲猜复现,依赖用户反馈。

查看以下案例:

// bad
fetchData().then(doSomethingMightThrowError).catch(console.log);

// good
fetchData()
 .then(doSomethingMightThrowError)
 .catch(error => {
   console.error(error);
   message.error(error.message);
   logger.log(error);
 });

Exceptions Hierarchy

使用自定义异常,让异常层次结构分明。

管理好业务代码中的异常是非常酷的一件事,上面章节有介绍到Javascript给我们提供的一些基础的异常类型,这些异常类型并不与我们的业务相关。所以使用这些异常来控制代码中的错误也显得不那么恰当,我们的代码正是对我们业务的建模。同样的,我们也要将与业务相关的这些异常建模管理,对异常进行语义化,并在业务逻辑发生特定情况时触发。否则就算调用方捕获了异常也不知道该如何去处理。

这样做往往会带来一些好处:

1、使用error instanceof CustomBizError更容易识别异常,会让判断逻辑更简洁且已读,更容易处理捕获到的异常并恢复程序。

2、通过标准化我们的自定义错误类,让我们更容易做上层处理,比如上面有提到的接口异常我可以选择不作为脚本异常全局上报,因为通常在接口异常里就已经上报了该信息;

参考以下例子


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('账户已存在!');
}

Provide context with exceptions

提供异常上下文

异常一旦发生了,一般都会有异常信息(message)和栈跟踪(stack trace)信息还有文件名之类的来定位发生错误的现场,但哪怕是这样在定位起来还是比较困难,所以一般建议去丰富异常信息让我们定位问题更加的快速。可以是在捕获到异常的地方解释我们的意图,同时这些额外的信息也都应该只是面向我们开发者用以定位问题,不需要让使用者去感知这些异常上下文,不在用户界面中进行体现。

结合上一条的自定义错误,我们还要为这些自定义错误提供更加丰富个上下文。

React中的建议

局部UI的JS Error不应该导致整个应用崩溃白屏,我们应该把他的影响范围控制在最小,这是一个容易形成共识的结论,于是React 16引入了错误边界(Error Boundaries)的概念。

React Error Boundaries 官方文档[2] 里提到:

错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。

ProComponents[3]的很多组件应该都有使用Error Boundaries比如ProTable,用以异常发生时只对局部UI产生影响,查看@ant-design/pro-utils中的源码可以看到和官网的处理别无二致,更多的信息查看官网有非常详细的介绍:


import { Result } from 'antd';
import type { ErrorInfo } from 'react';
import React from 'react';

// eslint-disable-next-line @typescript-eslint/ban-types
class 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-console
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <Result status="error" title="Something went wrong." extra={this.state.errorInfo} />;
    }
    return this.props.children;
  }
}

export { ErrorBoundary };

所以给我们的启示是组件库或者业务系统中的块级的一些东西(spm模型中的c位)一定要考虑好组件级别的异常处理。

异常的全局上报

基本上这是对付不可预知异常的终极解法,自动收集错误报告并在达到阈值时做出告警,属于在理想情况下异常发生后能让研发同学们能第一时间发现并定位解决问题,主要会使用2个全局事件:

window.onerror事件

JS运行中的大部分异常(包括语法错误),都会触发window上的error事件执行注册的函数,不同于try catch,onerror既可以感知同步异常也可以感知异步任务的异常(除了promise异常),使用方法如下:


// message:错误信息(字符串)。
// source:发生错误的脚本URL(字符串)
// lineno:发生错误的行号(数字)
// colno:发生错误的列号(数字)
// error:Error对象(对象)
window.onerror = function(message, source, lineno, colno, error) {
   logger.log('捕获到异常:',{ message, source, lineno, colno, error });
}

unhandledrejection事件

作为以上方案的补充版,promise异常的捕获依赖于全局注册unhandledrejection,使用方法如下

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/

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/QXUphQ7hbudggwYXZTIisw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Java 中验证时间格式的 4 种方法 2年以前  |  3810次阅读
Java经典面试题答案解析(1-80题) 4年以前  |  3595次阅读
CentOS 配置java应用开机自动启动 4年以前  |  2761次阅读
IDEA依赖冲突分析神器—Maven Helper 4年以前  |  2747次阅读
SpringBoot 控制并发登录的人数教程 4年以前  |  2424次阅读
 目录