对大多数开发者来说,最自然的错误处理形式是同步的try..catch结构。不幸的是,它仅能用于同步状态,所以在异步代码模式中它帮不上什么忙:

    能有try..catch当然很好,但除非有某些附加的环境支持,它无法与异步操作一起工作。我们将会在第四章中讨论generator时回到这个话题。

    在回调中,对于错误处理的模式已经有了一些新兴的模式,最有名的就是“错误优先回调”风格:

    1. function foo(cb) {
    2. setTimeout( function(){
    3. try {
    4. var x = baz.bar();
    5. cb( null, x ); // 成功!
    6. }
    7. catch (err) {
    8. cb( err );
    9. }
    10. }, 100 );
    11. }
    12. if (err) {
    13. console.error( err ); // 倒霉 :(
    14. }
    15. else {
    16. console.log( val );
    17. }
    18. } );

    注意: 这里的try..catch仅在baz.bar()调用立即地,同步地成功或失败时才能工作。如果baz.bar()本身是一个异步完成的函数,它内部的任何异步错误都不能被捕获。

    我们传递给foo(..)的回调期望通过预留的err参数收到一个表示错误的信号。如果存在,就假定出错。如果不存在,就假定成功。

    这类错误处理在技术上是 异步兼容的,但它根本组织的不好。用无处不在的语句检查将多层错误优先回调编织在一起,将不可避免地将你置于回调地狱的危险之中(见第二章)。

    那么我们回到Promise的错误处理,使用传递给then(..)的拒绝处理器。Promise不使用流行的“错误优先回调”设计风格,反而使用“分割回调”的风格;一个回调给完成,一个回调给拒绝:

    虽然这种模式表面上看起来十分有道理,但是Promise错误处理的微妙之处经常使它有点儿相当难以全面把握。

    考虑下面的代码:

    1. var p = Promise.resolve( 42 );
    2. p.then(
    3. function fulfilled(msg){
    4. // 数字没有字符串方法,
    5. // 所以这里抛出一个错误
    6. console.log( msg.toLowerCase() );
    7. },
    8. // 永远不会到这里
    9. }
    10. );

    如果msg.toLowerCase()合法地抛出一个错误(它会的!),为什么我们的错误处理器没有得到通知?正如我们早先解释的,这是因为 这个 错误处理器是为ppromise准备的,也就是已经被值42完成的那个promise。ppromise是不可变的,所以唯一可以得到错误通知的promise是由p.then(..)返回的那个,而在这里我们没有捕获它。

    这应当解释了:为什么Promise的错误处理是易错的。错误太容易被吞掉了,而这很少是你有意这么做的。

    警告: 如果你以一种不合法的方式使用Promise API,而且有错误阻止正常的Promise构建,其结果将是一个立即被抛出的异常,而不是一个拒绝Promise。这是一些导致Promise构建失败的错误用法:new Promise(null)Promise.all()Promise.race(42)等等。如果你没有足够合法地使用Promise API来首先实际构建一个Promise,你就不能得到一个拒绝Promise!

    几年前Jeff Atwood曾经写到:编程语言总是默认地以这样的方式建立,开发者们会掉入“绝望的深渊”( )——在这里意外会被惩罚——而你不得不更努力地使它正确。他恳求我们相反地创建“成功的深渊”,就是你会默认地掉入期望的(成功的)行为,而如此你不得不更努力地去失败。

    为了回避把一个被遗忘/抛弃的Promise的错误无声地丢失,一些开发者宣称Promise链的“最佳实践”是,总是将你的链条以catch(..)终结,就像这样:

    因为我们没有给then(..)传递拒绝处理器,默认的处理器会顶替上来,它仅仅简单地将错误传播到链条的下一个promise中。如此,在p中发生的错误,与在p之后的解析中(比如msg.toLowerCase())发生的错误都将会过滤到最后的handleErrors(..)中。

    问题解决了,对吧?没那么容易!

    要是本身也有错误呢?谁来捕获它?这里还有一个没人注意的promise:catch(..)返回的promise,我们没有对它进行捕获,也没注册拒绝处理器。

    你不能仅仅将另一个catch(..)贴在链条末尾,因为它也可能失败。Promise链的最后一步,无论它是什么,总有可能,即便这种可能性逐渐减少,悬挂着一个困在未被监听的Promise中的,未被捕获的错误。

    听起来像一个不可解的迷吧?

    这不是一个很容易就能完全解决的问题。但是有些接近于解决的方法,或者说 更好的方法

    一些Promise库有一些附加的方法,可以注册某些类似于“全局的未处理拒绝”的处理器,全局上不会抛出错误,而是调用它。但是他们识别一个错误是“未被捕获的错误”的方案是,使用一个任意长的计时器,比如说3秒,从拒绝的那一刻开始计时。如果一个Promise被拒绝但没有错误处理在计时器被触发前注册,那么它就假定你不会注册监听器了,所以它是“未被捕获的”。

    实践中,这个方法在许多库中工作的很好,因为大多数用法不会在Promise拒绝和监听这个拒绝之间有很明显的延迟。但是这个模式有点儿麻烦,因为3秒实在太随意了(即便它是实证过的),还因为确实有些情况你想让一个Promise在一段不确定的时间内持有它的拒绝状态,而且你不希望你的“未捕获错误”处理器因为这些误报(还没处理的“未捕获错误”)而被调用。

    另一种常见的建议是,Promise应当增加一个done(..)方法,它实质上标志着Promise链的“终结”。done(..)不会创建并返回一个Promise,所以传递给done(..)的回调很明显地不会链接上一个不存在的Promise链,并向它报告问题。

    那么接下来会发什么?正如你通常在未处理错误状态下希望的那样,在done(..)的拒绝处理器内部的任何异常都作为全局的未捕获错误抛出(基本上扔到开发者控制台):

    1. var p = Promise.resolve( 42 );
    2. p.then(
    3. function fulfilled(msg){
    4. // 数字没有字符串方法,
    5. // 所以这里抛出一个错误
    6. console.log( msg.toLowerCase() );
    7. }
    8. )
    9. .done( null, handleErrors );
    10. // 如果`handleErrors(..)`自身发生异常,它会在这里被抛出到全局

    这听起来要比永不终结的链条或随意的超时要吸引人。但最大的问题是,它不是ES6标准,所以不管听起来多么好,它成为一个可靠而普遍的解决方案还有很长的距离。

    那我们就卡在这里了?不完全是。

    注意: 在写作本书的时候,Chrome和Firefox都早已试图实现这种“未捕获拒绝”的能力,虽然至多也就是支持的不完整。

    然而,如果一个Promise不被垃圾回收——通过许多不同的代码模式,这极其容易不经意地发生——浏览器的垃圾回收检测不会帮你知道或诊断你有一个拒绝的Promise静静地躺在附近。

    还有其他选项吗?有。

    以下讲的仅仅是理论上,Promise 可能 在某一天变成什么样的行为。我相信那会比我们现在拥有的优越许多。而且我想这种改变可能会发生在后ES6时代,因为我不认为它会破坏Web的兼容性。另外,如果你小心行事,它是可以被填补(polyfilled)/预填补(prollyfilled)的。让我们来看一下:

    • 如果你希望拒绝的Promise在被监听前,将其拒绝状态保持一段不确定的时间。你可以调用defer(),它会压制这个Promise自动报告错误。

    如果一个Promise被拒绝,默认地它会吵吵闹闹地向开发者控制台报告这个情况(而不是默认不出声)。你既可以选择隐式地处理这个报告(通过在拒绝之前注册错误处理器),也可以选择明确地处理这个报告(使用defer())。无论哪种情况, 都控制着这种误报。

    考虑下面的代码:

    我们创建了p,我们知道我们会为了使用/监听它的拒绝而等待一会儿,所以我们调用defer()——如此就不会有全局的报告。defer()单纯地返回同一个promise,为了链接的目的。

    foo(..)返回的promise 当即 就添附了一个错误处理器,所以这隐含地跳出了默认行为,而且不会有全局的关于错误的报告。

    但是从then(..)调用返回的promise没有defer()或添附错误处理器,所以如果它被拒绝(从它内部的任意一个解析处理器中),那么它就会向开发者控制台报告一个未捕获错误。

    这种设计称为成功的深渊。默认情况下,所有的错误不是被处理就是被报告——这几乎是所有开发者在几乎所有情况下所期望的。你要么不得不注册一个监听器,要么不得不有意什么都不做,并指示你要将错误处理推迟到 稍后;你仅为这种特定情况选择承担额外的责任。

    这种方式唯一真正的危险是,你defer()了一个Promise但是实际上没有监听/处理它的拒绝。

    但你不得不有意地调用来选择进入绝望深渊——默认是成功深渊——所以对于从你自己的错误中拯救你这件事来说,我们能做的不多。

    我觉得对于Promise的错误处理还有希望(在后ES6时代)。我希望上层人物将会重新思考这种情况并考虑选用这种方式。同时,你可以自己实现这种方式(给读者们的挑战练习!),或使用一个 聪明 的Promise库来为你这么做。