最佳的JavaScript错误处理实践


Posted in Javascript onJuly 16, 2016

不管你的技术水平如何,错误或异常是应用程序开发者生活的一部分。Web开发的不连贯性留下了许多错误能够发生并确实已经发生的地方。解决的关键在于处理任何不可预见的(或可预见的错误),来控制用户的体验。利用JavaScript,就有多种技术和语言特色可以用来正确地解决任何问题。

在 JavaScript 中处理错误很危险。如果你相信墨菲定律,会出错的终究会出错!在这篇文章中,我会深入研究 JavaScript 中的错误处理。我会涉及到一些陷阱和好的实践。最后我们会讨论异步代码处理和 Ajax。

我认为 JavaScript 的事件驱动模型给这门语言添加了丰富的含义。我认为这种浏览器的事件驱动引擎和报错机制没什么区别。每当发生错误,就相当于在某个时间点抛出一个事件。理论上说,我们在 JavaScript 中可以像处理普通事件一样去处理抛错事件。如果对你来说这听起来很陌生,那请集中注意力开始学习下面的旅程。本文只针对客户端的 JavaScript。

示例

本文章中用到的代码示例在 GitHub 上可以得到,目前页面是这个样子的:

最佳的JavaScript错误处理实践
 

单击每个按钮都会引发一个错误。它模拟产生一个 TypeError 型的 exception。下面是对这样一个模块的定义及单元测试。

function error() {
 var foo = {};
 return foo.bar();
}

首先,这个函数定义了一个空的对象 foo。请注意,bar() 方法没有在任何地方定义。我们用单元测试来验证这确实会引发报错。

it('throws a TypeError', function () {
 should.throws(target, TypeError);
});

这个单元测试使用 Mocha 和 Should.js 库中的测试断言。Mocha 是一个运行测试框架,should.js 是一个断言库。如果你不太熟悉,可以在线免费浏览他们的文档。一个测试用例通常以 it('description') 开始,以 should 中断言的通过或者失败结束。用这套框架的好处就是可以在 node 里进行单元测试,而不必非在浏览器里。我建议大家认真对待这些测试,因为它们验证了 JavaScript 中很多关键的基本概念。

如上所示, error() 定义了一个空对象,然后试图去调用其中的方法。因为在这个对象中不存在 bar() 这个方法,它会抛出一个异常。相信我,在像 JavaScript 这种动态语言里,任何人都有可能犯这类错误。

不好的示范

先来看看不佳的错误处理方式。我处理错误的动作抽象出来,绑定在按钮上。下面是处理程序的单元测试的样子:

function badHandler(fn) {
 try {
  return fn();
 } catch (e) { }
 return null;
}

这个处理函数接收一个回调函数 fn 作为依赖。接着在处理程序的内部调用了这个函数。这个单元测试示例了如何使用这个方法。

it('returns a value without errors', function() {
 var fn = function() {
  return 1;
 };
 var result = target(fn);
 result.should.equal(1);
});

it('returns a null with errors', function() {
 var fn = function() {
  throw Error('random error');
 };
 var result = target(fn);
 should(result).equal(null);
});

就像你看到的那样,如果发生了错误,这个诡异的处理方法会返回一个 null。这个回调函数 fn() 会指向一个合法的方法或者错误。下面的单击处理事件完成了剩下的部分。

(function (handler, bomb) {
 var badButton = document.getElementById('bad');

 if (badButton) {
  badButton.addEventListener('click', function () {
   handler(bomb);
   console.log('Imagine, getting promoted for hiding mistakes');
  });
 }
}(badHandler, error));

糟糕的是我刚刚得到的是个 null。这让我在想确定到底发生了什么错误的时候非常迷茫。这种发生错误就沉默的策略覆盖了从用户体验设计到数据损坏的各个环节。随之而来令人沮丧的一面就是,我必须花费好几个小时调试但是却看不到 try-catch 代码块里的错误。这种诡异的处理隐藏掉了代码中所有的报错,它假设一切都是正常的。这在某些不注重代码质量的团队中,能够顺利的执行。但是,这些被隐藏的错误最终会迫使你花几个小时来调试代码。在一种依赖于调用栈的多层解决方案中,有可能可以确定错误来自于何处。可能在极少数情况下对 try-catch 做故障静默处理是合适的。但是如果遇到错误就去处理,也不是一个好方案。

这种失败即沉默的策略会促使你在代码中对错误做更好的处理。JavaScript 提供了更优雅的方式来处理这类问题。

不易读的方案

继续,接下来来看看不太好理解的处理方式。我将会跳过与 DOM 紧耦合的部分。这部分与我们刚刚看过的不好的处理方式没什么不同。重点是下面单元测试中处理异常的部分。

function uglyHandler(fn) {
 try {
  return fn();
 } catch (e) {
  throw Error('a new error');
 }
}

it('returns a new error with errors', function () {
 var fn = function () {
  throw new TypeError('type error');
 };
 should.throws(function () {
  target(fn);
 }, Error);
});

比起刚刚不好的处理方式,有一个很好的进步。异常在调用堆栈中被抛出。我喜欢的地方是错误从堆栈中解放出来,这对于调试有巨大的帮助。抛出一个异常,解释器就会在调用堆栈中一级级查看找到下一个处理函数。这就提供了很多机会在调用堆栈的顶层去处理错误。不幸的是,因为他是一种不太好理解的错误,我看不到了原始错误的信息。所以我必须沿着调用栈找过去,找到最原始的异常。但是至少我知道抛出异常的地方发生了一个错误。

这种不易读的错误处理虽然无伤大雅但是却使得代码难以理解。让我们看看浏览器如何处理错误的。

调用栈

那么,抛出异常的一种方式就是在调用堆栈的顶层添加 try...catch 代码块。比如说:

function main(bomb) {
 try {
  bomb();
 } catch (e) {
  // Handle all the error things
 }
}

但是,记得我说过浏览器是事件驱动的吗?是的,JavaScript 中的一个异常不过就是一个事件。解释器会在发生异常当前的上下文处停止程序,并抛出异常。为了证实这一点,下面写了一个我们能够看到的全局的事件处理函数 onerror。它看上去就是这个样子:

window.addEventListener('error', function (e) {
 var error = e.error;
 console.log(error);
});

这个事件处理函数在执行环境中捕获错误。错误事件会在各种各样的地方产生各种错误。这种方式的重点是在代码中集中处理错误。就像其他的事件一样,你可以用一个全局的处理函数去处理各种不同的错误。这使得错误处理只有一个单一的目标,如果你遵守 SOLID (single responsibility 单一职责, open-closed 开闭, Liskov substitution 代换, interface segregation 界面分离 and dependency inversion 依赖倒置) 原则。你可以在任何时候注册错误处理函数。解释器会循环执行这些函数。代码从充满 try...catch 的语句中解放出来,变得易于调试。这种做法的关键是像处理 JavaScript 普通事件一样处理发生的错误。

现在,有了一种方法,用全局处理函数来显示出调用栈,我们可以用它来做什么?终究,我们要利用调用栈。

记录下调用栈

调用栈在处理修复 bug 上非常有用。好消息是浏览器提供了这个信息。就算目前,error 对象的 stack 属性并不是标准,但是在比较新的浏览器里都普遍支持这个属性。

所以,我们能够做的很酷的事情就是把它给服务器打印出来:

window.addEventListener('error', function (e) {
 var stack = e.error.stack;
 var message = e.error.toString();
 if (stack) {
  message += '\n' + stack;
 }
 var xhr = new XMLHttpRequest();
 xhr.open('POST', '/log', true);
 xhr.send(message);
});

在代码示例中可能不太明显,但这个事件处理程序会被前面的错误代码触发。如上所述,每个处理程序都有一个单一的目的,它使代码 DRY(don't repeat yourself 不重复制造轮子)。我感兴趣的是如何在服务器上捕获这些消息。

下面是 node 运行时的截图:

最佳的JavaScript错误处理实践
 

调用堆栈对调试代码很有帮助。永远不要低估调用栈的作用。

异步处理

哦,处理异步代码相当危险!JavaScript 将异步代码从当前的执行环境中带出来。这意味着下面这种 try...catch 语句有个问题。

function asyncHandler(fn) {
 try {
  setTimeout(function () {
   fn();
  }, 1);
 } catch (e) { }
}

这个单元测试还有剩下的部分:

it('does not catch exceptions with errors', function () {
 var fn = function () {
  throw new TypeError('type error');
 };
 failedPromise(function() {
  target(fn);
 }).should.be.rejectedWith(TypeError);
});

function failedPromise(fn) {
 return new Promise(function(resolve, reject) {
  reject(fn);
 });
}

我必须用一个 promise 来结束这个处理程序,以验证异常。注意,尽管我的代码都在 try...catch 中,但是还是出现了未处理的异常。是的,try...catch 只在一个单独的执行环境中有作用。当异常被抛出时,解释器的执行环境已经不是当前的 try-catch 块了。这一行为的发生与 Ajax 调用相似。所以,现在有了两种选择。一种可选方案就是在异步回调中捕捉异常:

setTimeout(function () {
 try {
  fn();
 } catch (e) {
  // Handle this async error
 }
}, 1);

这种方法虽然有用,但是还有很大的提升空间。首先,try...catch 代码块在代码中处处出现。事实上,上世纪 70 年代编程调用,他们希望他们的代码能够回退。另外,V8 引擎不鼓励 在函数中使用 try…catch 代码块 (V8 是 Chrome 浏览器和 Node 使用的 JavaScript 引擎)。他们推荐在调用堆栈顶层写这些捕获异常的代码块。

所以,这告诉我们什么?我上面说过的,在任何执行上下文中的全局错误处理程序是有必要的。如果你将一个错误处理程序添加到 window 对象,那就是说,您已经完成了!遵守 DRY 和 SOLID 的原则不是很好吗?一个全局错误处理程序将保持你的代码易读和干净。

下面就是服务器端异常处理打印的报告。注意,如果你使用的示例中的代码,输出的内容可能会根据你使用的浏览器不同有少许不同。

 最佳的JavaScript错误处理实践

这个处理函数甚至可以告诉我哪个错误是出自于异步代码。它告诉我错误来自于 setTimeout() 处理函数。太酷了!

错误是每一个应用程序的一部分,但是适当的错误处理却不是。在处理错误这件事上至少有两种方法。一种是失败即沉默的方案,即在代码中忽略错误。另一种是快速发现和解决错误的方法,即在错误处停止并且重现。我想我已经把我赞成哪一种及为什么赞成表达地很清楚。我的选择:不要隐藏问题。没有人会为你程序中的意外事件去指责你。这是可以接受的,去打断点、重现、给用户一个尝试。在一个并不完美的世界中,给自己一个机会是很重要的。错误是不可避免的,为了解决错误你做的事情才是重要的。合理地运用JavaScript的错误处理特色和自动灵活的译码可以使用户的体验更顺畅,同时也让开发方的诊断工作变得更轻松。

Javascript 相关文章推荐
JavaScript效率调优经验
Jun 04 Javascript
js 多种变量定义(对象直接量,数组直接量和函数直接量)
May 24 Javascript
仿新浪微博返回顶部的jquery实现代码
Oct 01 Javascript
jquery仿京东导航/仿淘宝商城左侧分类导航下拉菜单效果
Apr 24 Javascript
JS打开层/关闭层/移动层动画效果的实例代码
May 11 Javascript
Javascript 完美运动框架(逐行分析代码,让你轻松了运动的原理)
Jan 23 Javascript
JS去除空格和换行的正则表达式(推荐)
Jun 14 Javascript
带有定位当前位置的百度地图前端web api实例代码
Jun 21 Javascript
js实现动态创建的元素绑定事件
Jul 19 Javascript
微信小程序左右滑动的实现代码
Dec 15 Javascript
vue组件间的参数传递实例详解
Apr 26 Javascript
Node.js开发之套接字(socket)编程入门示例
Nov 05 Javascript
JS+CSS3实现超炫的散列画廊特效
Jul 16 #Javascript
js canvas实现擦除动画
Jul 16 #Javascript
微信jssdk用法汇总
Jul 16 #Javascript
详解JavaScript节流函数中的Throttle
Jul 16 #Javascript
很棒的js选项卡切换效果
Jul 15 #Javascript
轻松5句话解决JavaScript的作用域
Jul 15 #Javascript
jQuery EasyUI基础教程之EasyUI常用组件(推荐)
Jul 15 #Javascript
You might like
在WordPress的后台中添加顶级菜单和子菜单的函数详解
2016/01/11 PHP
PHP重定向与伪静态区别
2017/02/19 PHP
PHP两种实现无级递归分类的方法
2017/03/02 PHP
PHP实现深度优先搜索算法(DFS,Depth First Search)详解
2017/09/16 PHP
JavaScript中this关键字使用方法详解
2007/03/08 Javascript
在多个页面使用同一个HTML片段的代码
2011/03/04 Javascript
Javascript实现字数统计
2015/07/03 Javascript
jQuery验证插件validate使用方法详解
2020/09/13 Javascript
Avalonjs 实现简单购物车功能(实例代码)
2017/02/07 Javascript
js实现多张图片延迟加载效果
2017/07/17 Javascript
angular实现页面打印局部功能的思考与方法
2018/04/13 Javascript
使用ng-packagr打包Angular的方法示例
2018/09/21 Javascript
vue+element加入签名效果(移动端可用)
2019/06/17 Javascript
JavaScript页面加载事件实例讲解
2019/09/01 Javascript
解决layui弹框失效的问题
2019/09/09 Javascript
如何优雅地在Node应用中进行错误异常处理
2019/11/25 Javascript
[42:22]DOTA2上海特级锦标赛C组小组赛#1 OG VS Archon第一局
2016/02/27 DOTA
[04:14]从西雅图到上海——玩家自制DOTA2主题歌曲应援TI9
2019/07/11 DOTA
bat和python批量重命名文件的实现代码
2016/05/19 Python
Python类和对象的定义与实际应用案例分析
2018/12/27 Python
python使用socket实现的传输demo示例【基于TCP协议】
2019/09/24 Python
linux 下selenium chrome使用详解
2020/04/02 Python
python不同版本的_new_不同点总结
2020/12/09 Python
如何用 Python 制作 GitHub 消息助手
2021/02/20 Python
西班牙英格列斯百货官网:El Corte Inglés
2016/09/25 全球购物
存储过程的优点有哪些
2012/09/27 面试题
行政主管岗位职责
2013/11/18 职场文书
简历的自我评价
2014/02/03 职场文书
房地产开盘策划方案
2014/02/10 职场文书
接待员岗位责任制
2014/02/10 职场文书
督导岗位职责范本
2015/04/10 职场文书
文明礼仪倡议书
2015/04/28 职场文书
驳回起诉裁定书
2015/05/19 职场文书
2015年个人实习工作总结
2015/05/28 职场文书
火锅店的开业营销方案范本!
2019/07/05 职场文书
厉害!这是Redis可视化工具最全的横向评测
2021/07/15 Redis