[六字真言]5.咪.功力不足,学习前端JavaScript异常

A Guide to Proper Error Handling in JavaScript

这是关于JavaScript中异常处理的故事。如果你相信 墨菲定律 ,那么任何事情都可能出错,不,一定会出错!这篇文章中我们来看下JavaScript中的出错处理。文章会覆盖异常处理使用的正反例,然后看下ajax的异步处理。

JavaScript的事件驱动机制让JavaScript更加丰富,浏览器好比就是一个事件驱动的机器,错误也是一种事件。当一个错误发生时,一个事件就在某个点抛出。理论上,有人会说错误是Javascript中的简单事件。如果你觉得是这样,那你就要好好去看看了。另外这篇文章只关注浏览器端的JavaScript的情况。

这篇文章将在《 Exceptional Exception Handling in JavaScript 》这篇文章的概念基础上进行解释。解释起来就是,当发生错误时,JavaScript会去调用栈检查异常事件。如果你对此不熟悉建议先去看看基础的东西。我们的目的是探索处理异常的必要性,接下来你会看到一个 try...catch 块语句,你要认真思考。

例子

例子的代码在 github 上,而且最终展示成这样:

所有的按钮点击是都会触发"炸弹",这个炸弹模拟了一个抛出的 TypeError 异常。下面是这个模块单元测试的定义:

  1. function error() {
  2. var foo = {};
  3. return foo.bar();
  4. }

开始时,这个函数定义了一个空的对象 foo ,注意 bar() 没有在任何地方定义,我们用一个测试用例来看下它是如何引爆炸弹的。

  1. it(‘throws a TypeError‘, function () {
  2. should.throws(target, TypeError);
  3. });

这个单元测试是用 mochashould.js 写的。 mocha 是一个测试框架, should.js 是一个断言库。如果你熟悉它们后,你会感觉写起来很爽。测试一般使用 it(‘description‘) 开始,然后在 should 中使用 pass/fail 结束。好消息是测试用例可以在node端运行而不需要浏览器。我建议多关注这些测试,因为它们能帮助我们提升代码的质量。

正如所显示的, error() 定义了一个空的对象,然后尝试访问一个方法,因为 bar() 方法在对象中不存在而会抛出一个异常。使用JavaScript这种动态语言运行一定会出错。

错误的方式

对于一些错误的处理,我从按钮的而事件中抽离出异常处理的方式,下面是单元测试函数的代码:

  1. function badHandler(fn) {
  2. try {
  3. return fn();
  4. } catch (e) { }
  5. return null;
  6. }

这个处理函数接收一个 fn 回调函数作为输入,这个函数然后在处理器函数里面被调用,单元测试如下:

  1. it(‘returns a value without errors‘, function() {
  2. var fn = function() {
  3. return 1;
  4. };
  5. var result = target(fn);
  6. result.should.equal(1);
  7. });
  8. it(‘returns a null with errors‘, function() {
  9. var fn = function() {
  10. throw Error(‘random error‘);
  11. };
  12. var result = target(fn);
  13. should(result).equal(null);
  14. });

如你所见,这个糟糕的处理函数如果有地方出错就会返回null,回调函数 fn() 可以指向一个正确的方法或者一个异常,下面的点击处理函数会显示最终的处理结果。

  1. (function (handler, bomb) {
  2. var badButton = document.getElementById(‘bad‘);
  3. if (badButton) {
  4. badButton.addEventListener(‘click‘, function () {
  5. handler(bomb);
  6. console.log(‘Imagine, getting promoted for hiding mistakes‘);
  7. });
  8. }
  9. }(badHandler, error));

可恶的是,这里返回了一个null,当我想找哪里出了问题时整个人都蒙逼了。这种失败沉默的方式会影响用户体验和数据混乱。更令人崩溃的是,我花了几个小时来进行debugg,但却没有使用 try-catch ,这个糟糕的处理函数吞没了错误并认为它没有问题, 这样继续执行下去不会降低代码质量,但是隐藏的错误未来会让你花几个小时来debugg。在一个多层的深调用时,基本上不可能发现哪里出了问题。而在这些少数的地方使用 try-catch 是正确的。但是一旦进入错误处理函数,就比较糟糕了。

失败沉默策略会让你不容易发现错误所在,JavaScript提供了一个更优雅的方式来处理这些问题。

比较差的方式

继续,是时候说下一个稍微好点的方法了。我先跳过事件绑定到dom上的部分。这个函数处理和刚刚我们看到的没什么不同。所不同的是单元测试中它处理异常的方式。

  1. function uglyHandler(fn) {
  2. try {
  3. return fn();
  4. } catch (e) {
  5. throw Error(‘a new error‘);
  6. }
  7. }
  8. it(‘returns a new error with errors‘, function () {
  9. var fn = function () {
  10. throw new TypeError(‘type error‘);
  11. };
  12. should.throws(function () {
  13. target(fn);
  14. }, Error);
  15. });

这里定义在原来的基础上改进了。这里异常事件在调用栈中进行冒泡,我喜欢的是现在错误现在会离开方便debugg的调用栈。在这个异常中,解释器会遍历整个栈寻找另一个错误处理函数。这样就可以有机会在调用栈的顶端处理这些错误。不幸的是,因为这个方法,我不知道错误是从哪个地方抛出来的。所以我又得反向遍历这个栈找到错误异常的源头。但至少我知道某个地方出错了,并能找到是哪个地方抛出的错误。

离开调用栈

所以,一个抛出异常处理的方法是直接调用栈的顶端使用 try-catch ,就像:

  1. function main(bomb) {
  2. try {
  3. bomb();
  4. } catch (e) {
  5. // Handle all the error things
  6. }
  7. }

但是,记住我说的浏览器是事件驱动的。是的,JavaScript中的错误也不过是一个事件。解释器在当前的执行上下文中执行后释放。结果是,我们可以利用一个 onerror 的全局异常事件处理函数,它大概是这样的:

  1. if(window.addEventListener){
  2. window.addEventListener(‘error‘, function (e) {
  3. var error = e.error;
  4. console.log(error);
  5. });
  6. }else if(window.attachEvent){
  7. window.attachEvent(‘onerror‘, function (e) {
  8. var error = e.error;
  9. console.log(error);
  10. });
  11. }else{
  12. window.onerror = function(e){
  13. var error = e.error;
  14. console.log(error);
  15. }
  16. }

感觉这种方式应该不错,可以尝试一下

这个处理函数能捕获任何执行上下文中的错误异常。包括任何类型的任何错误。而且它能定位到代码中的错误处理。就像其它任何事件一样,你能捕获特定错误的具体信息。这样能使异常处理器只专注于一件事情,如果你允许这样做的话。这些处理函数也可以在任何时候注册,解释器会尽可能的遍历更多的处理函数,我们再也不用使用 try-catch 块这种带有瑕疵的debug方式了。尤其是在对待像JavaScript这类事件驱动机制的语言时,onerror的优势就更大了

现在我们可以使用全局处理函数来离开栈了,我们可以用来干什么呢。毕竟,调用栈还是存在的。

捕获栈信息

调用栈在定位问题时超级有用。好消息是,浏览器提供了这个信息。理所当然,查看错误异常中的栈属性不是标准的一部分,但是只在新的浏览器中可以使用。所以,你就可以这样来把错误日志发送给服务器了。

  1. window.addEventListener(‘error‘, function (e) {
  2. var stack = e.error.stack;
  3. var message = e.error.toString();
  4. if (stack) {
  5. message += ‘\n‘ + stack;
  6. }
  7. var xhr = new XMLHttpRequest();
  8. xhr.open(‘POST‘, ‘/log‘, true);
  9. xhr.send(message);
  10. });

可能从代码样例来说不是很明显,但是上面的代码一定会出错。上面提到了,每个处理函数都只处理一个功能。我关心的是这些信息是怎样被服务器捕获的。如下:

这些信息来自FireFox 46的开发版本,通过一个正确的错误处理函数,记录了出错的情况。这里没必要隐藏错误,我可以看到什么地方出现的什么错误。这样代码debugg就很爽了。这些信息也可以保存在持续化缓存中以便于以后分析。

调用栈对于debugg来说是很有用的,永远不要低估调用栈的力量。

异步处理

处理异步时,JavaScript的异步处理代码不在当前的指向上下文中,这意味着 try-catch 语句会有问题(不能捕获到异常):

  1. function asyncHandler(fn) {
  2. try {
  3. setTimeout(function () {
  4. fn();
  5. }, 1);
  6. } catch (e) { }
  7. }

单元测试的结果如下:

  1. it(‘does not catch exceptions with errors‘, function () {
  2. var fn = function () {
  3. throw new TypeError(‘type error‘);
  4. };
  5. failedPromise(function() {
  6. target(fn);
  7. }).should.be.rejectedWith(TypeError);
  8. });
  9. function failedPromise(fn) {
  10. return new Promise(function(resolve, reject) {
  11. reject(fn);
  12. });
  13. }

我必须用promise包含这个处理器来获取这个错误。注意的是,一个未被处理的异常发生时,尽管我将代码使用 try-catch 包含起来了,是的, try-catch 只能在单一的作用域内有效。在一个异常被抛出的同时,解释器就会从 try-catch 中离开,ajax也是一样的。所以有两种选择,一种是在异步调用里面捕获异常:

  1. setTimeout(function () {
  2. try {
  3. fn();
  4. } catch (e) {
  5. // Handle this async error
  6. }
  7. }, 1);

这种方法很有效,但是很多地方可以改进。首先, try-catch 块在这里用很混乱。实际上,之前是这么做的,但是有问题。另外,V8引擎不鼓励 在函数中使用try-catch (V8 是chrome和nodejs中的JavaScript引擎)。它们的建议是最外层写这些块。

所以我们该怎么办?我说过全局异常处理可以在任何执行上下文中执行,如果给window对象增加一个错误处理函数,就OK了。这样是不是既能处理捕获处理错误又能保持代码的优雅呢。全局的错误处理能让你的代码干净整洁。

下面是服务器收集到的错误日志,注意的是如果你使用同样的代码再不同浏览器上执行,你会看到收集到的日志也是不同的:

这个处理函数甚至告诉我们错误是从异步代码中抛出的吗,它告诉我们来至 setTimeout() 函数。

结论

总得来说,进行异常处理至少有两种方法。

  • 一个是失败沉默的方法,在错误发生时忽略错误不作为而不影响后面的继续执行。
  • 另一种是发生后迅速找到错误发生的地方。

明显我们知道那种方法更具有优势。我的选择是:不要隐藏错误。没人会因为你代码中有问题而鄙视你,用户多试一次是可以接受的。代码距离完美是很远的,错误也是不可避免的,重要的是你发现错误后会怎么做。

译者注:文章浅显的分析了错误处理的方式和一些正反案例,其实处理错误的最终目的还是提供前端代码的质量,关于错误处理上报可以参考下 badjs 的思路,基于现代前端开发模块化的基础,使用全局 onerrortry-catch 相结合的方式更能有效进行错误定位。



原文作者:Camilo Reyes

原译:ouven

原文地址:http://www.sitepoint.com/proper-error-handling-javascript/

这篇文章有点难度或者说不好理解

看自己的情况继续努力,过一段时间之后再来读一遍

来自为知笔记(Wiz)

时间: 2024-11-19 20:58:37

[六字真言]5.咪.功力不足,学习前端JavaScript异常的相关文章

诚念六字真言的一点体悟

六字真言: 嗡(ong)嘛(ma)呢(ni)呗(bei)咪(mei)吽(hong) 中文的发音有多种,但其实这并不重要,按自己的感觉和喜欢的念就行, 念这个,其实我也没有什么正式规矩怎样怎样的念,有时候心念或听乐曲上的. 后来偶有做恶梦,在梦中我竟然也能念起,然后便恐惧退散了!~ 呵呵,这不管是心理作用还是其他什么,总觉得应该是不错的,毕竟此咒含有诸佛无尽的加持和慈悲,是诸佛 慈悲和智慧的音声显现,此咒即是观世音菩萨的微妙本心! 嗡嘛呢呗咪吽

[六字真言]2.嘛.异常定制和通用.md

幻世当空 恩怨休怀 舍悟离迷 六尘不改 且怒且悲且狂哉! 最近一直在循环的一首歌! 丰富自己,比取悦他人更有力量.种下梧桐树,引得凤凰来.你若盛开,蝴蝶自来! 言归正传! 言归正传! 不要去大包大揽 人懒就是动力,我上课的时候为了图省事,经常利用 Exception 捕捉所有潜在的异常这里更正一下,这样的思想不对的,人的能力和经历是有限的,不要让他成为万能的! publicvoid update(User user){ try{ // 假设场景 //…抛出 IOException 的代码调用 /

[六字真言]3.呢.异常的谎言,你要相信多少次?

"嘛,呢",梵文意为"如意宝",表示"宝部心",又叫嘛呢宝,其实就是有"聚宝"的意思! 现在的社会现在的人,都是喜欢虚的假的,不喜欢真的诚的,谁虚伪谁高人一等,谁真诚谁傻瓜一个,这句话很现实的,会做的不如会说的. 这就是现实,真情可贵,用心陪醉,虚伪面对,从容领会,我就是永远都学不会 不知道最近心是怎么样了,周围发生了一些固有的定律,其实知道,只是不说!有的时候感觉大家都是聪明人,但是都在做不聪明的事情! 重复多次的"

[六字真言]4.叭.SpringMVC异常痛苦

"叭",除畜生道劳役之苦: 在学过的三阶段的时候,我们对SpringMVC的异常处理,一直可以算是简单中透着暴力,不要不重视异常!真的很重要,不要让它处在尴尬的位置! 在二阶段或者说三阶段中,技术一方面其中我认为最关键的地方是MVC思想的理解,对MVC理解程度决定着你学习过程中的难易程度,虽然有点夸张! 例如,当我们产生了一个 非业务异常 ,或者 非本业务可以处理的其他业务异常 ,那么我们一般会一直往 上层抛 (或者适当包装后继续抛出)直到 控制层 [我们就是这么解决问题,不知道你是否

[六字真言]6.吽.SpringMVC中上传大小异常填坑

最近在讲课的时候,遇到了关于上传文件过大的时候浏览器无法响应的问题,配置了捕获异常,有的学生浏览器好使,有的学生浏览器不好用!莫名其妙! MaxUploadSizeExceededException进入了无限的死循环,╮(╯▽╰)╭ 悲催! 为什么呢? 配置上传信息 在springmvc的核心配置文件配置如下信息 <!-- 5.配置上传 --> <bean id="multipartResolver" class="org.springframework.w

从零开始学习前端JAVASCRIPT — 1、JavaScript基础

1:定义:javascript是一种弱类型.动态类型.解释型的脚本语言. 弱类型:类型检查不严格,偏向于容忍隐式类型转换. 强类型:类型检查严格,偏向于不容忍隐式类型转换. 动态类型:运行的时候执行类型检查. 静态类型:编译的时候就知道每个变量的类型. 解释型:程序不需要编译,程序在运行的时候才翻译成机器语言,每执行一次都要翻译一次,因此效率比较低,但是跨平台性好. 编译型:程序在执行之前需要一个专门的翻译过程,把程序编译为机器语言的文件,运行时直接使用编译的结果就行了. 标记语言:标记语言的存

从零开始学习前端JAVASCRIPT — 5、JavaScript基础ES5

1:ES5简介 ECMAScript 5.1 (或仅 ES5) 是ECMAScript(基于JavaScript的规范)标准的修正. 与HTML5规范进程本质类似,ES5通过对现有JavaScript方法添加语句和原生ECMAScript对象做合并实现标准化. 2:严格模式的意义 相对于正常模式而言: 1:消除Javascript语法的一些不合理.不严谨之处,减少一些怪异行为. 2:消除代码运行的一些不安全之处,保证代码运行的安全. 3:提高编译器效率,增加运行速度. 4:为未来新版本的Java

从零开始学习前端JAVASCRIPT — 6、JavaScript基础DOM

1:DOM(Document  Object  Model)的概念和作用 document对象是DOM核心对象:对html中的内容,属性,样式进行操作. 节点树中节点之间的关系:父子,兄弟. 2:DOM常用属性 title:返回或设置当前文档的标题. all:返回所有元素的集合. forms:返回对文档中所有form对象的引用. 通过集合来访问相应的对象: 1.通过下标的形式. 2.通过name形式. 3:DOM查询方法 1.getElementById(id):返回拥有指定id的(第一个)对象

从零开始学习前端JAVASCRIPT — 4、JavaScript基础Math和Date对象的介绍

Math对象的介绍 1:Math对象 Math 对象用于执行数学任务.并不像 Date 和 String 那样是对象的类,因此没有构造函数 Math().您无需创建它,通过把 Math 作为对象使用就可以调用其所有属性和方法. 2:Math属性 PI:返回圆周率(约等于3.14159). 3:Math方法 Math.round(3.6);   // 四舍五入. Math.random();     // 返回大于等于0到小于1之间的随机数. // 随机数如何设定范围 // 0 - 100(包含)