继续,接下来来看看不太好理解的处理方式。我将会跳过与 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 将异步代码从当前的执行环境中带出来。这意味着下面这种 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);