28 August 2013

异常和 try/catch

当执行可能失败的操作时,异常和 try/catch 是一种直观的操作。通过这种方式,我们可以从失败中恢复,也可以不捕获异常,或者明确地将异常再次抛出,让异常沿着调用栈向上传播到调用者。

下面是一个简单的例子:

function thisMightFail() {
    //...
    if(badThingsHappened) {
        throw new Error(...);
    }

    return theGoodResult;
}

function recoverFromFailure(e) {
    //...
    return recoveryValue;
}

function getTheResult() {

    var result;

    try {
        result = thisMightFail();
    } catch(e) {
        result = recoverFromFailure(e);
    }

    return result;
}

在这个例子中,当 thisMightFail 失败并抛出一个 Error 时,getTheResult 捕获抛出的 Error,并调用 recoverFromFailure(例如,可以返回某个默认结果)。这个例子之所以能够工作,是因为 thisMightFail 是同步的。

面向异步

如果 thisMightFail异步的会如何呢?例如,它可能执行一个异步的 XHR 来获取数据:

function thisMightFail(callback, errback) {
    xhrGet('/result', callback, errback);
}

现在使用 try/catch 是不可能的了,我们必须提供一个 callback 和 errback 来处理成功和失败的情况。这在 JavaScript 应用程序中相当常见,所以没什么大不了的,真是这样吗?先别急,现在 getTheResult 也需要改变:

function getTheResult(callback) {

    // Simulating the catch-and-recover behavior of try/catch
    thisMightFail(callback, function(e) {

        var result = recoverFromFalure(e);
        callback(result);

    });

}

现在,对最终执行结果感兴趣的调用方,必须至少为每个函数签名增加 callback(也可能是 errback,请继续往下读)。

更多异步

如果 recoverFromFailure 也是异步的,我们不得不再添加一层嵌套的回调函数:

function getTheResult(callback) {

    // Simulating the catch-and-recover behavior of try/catch
    thisMightFail(callback, function(e) {

        recoverFromFailure(callback, function(e) {
            // What do we do here?!?!
        });

    });
}

这就提出了另一个问题:如果 recoverFromFailure 失败了该如何处理呢?当使用同步的 try/catch 时,recoverFromFailure 可以简单的抛出一个 ErrorError 将传播到调用 getTheResult 的代码。为了处理异步失败,我们不得不引入另一个 errback,这就导致在从 recoverFromFailure 到调用方的路径上,callbackerrback 侵入了每个函数的签名,而且调用方必须提供它们。

这也可能意味着我们不得不检查是否真地提供了 callback 和 errback,以及它们是否会抛出异常:

function thisMightFail(callback, errback) {
    xhrGet('/result', callback, errback);
}

function recoverFromFailure(callback, errback) {
    recoverAsync(
        function(result) {
            if(callback) {
                try {
                    callback(result);
                } catch(e) {
                    // Ok, callback threw an exception, let's switch to errback
                    // At least this will let the caller know that something went wrong.
                    // But now, both the callback and errback will have been called, and
                    // and the developer may not have accounted for that!
                    errback(e);
                }
            }
        },
        function(error) {
            if(errback) {
                try {
                    errback(error);
                } catch(ohnoes) {
                    // What do we do now?!?
                    // We could re-throw or not catch at all, but no one can catch
                    // the exception because this is all asynchronous.
                    // And now console.error has infiltrated deep into our code, too!
                    console.error(ohnoes);
                }
            }
        }
    );
}

function getTheResult(callback, errback) {

    // Simulating the catch-and-recover behavior of try/catch
    thisMightFail(callback, function(e) {

        recoverFromFailure(callback, errback);

    });

}

这段代码已经从一个简答的 try/catch 变为深度嵌套的回调函数,每个函数的签名需要增加 callbackerrback,需要增加额外的逻辑来检查是否可以安全地调用它们,而且讽刺的是,需要用两个 try/catch 块来确保 recoverFromFailure 真的可以从失败中恢复。

如何处理 finally?

试想一下,如果我们再将 finally 引入这种混乱的实现方式,事情必然会变得更复杂。基本上有两种选择:1) 为所有方法的签名增加一个 alwaysback 回调函数,并做相应的检查以确保可以安全地调用它,或者 2) 在 callback/errback 的内部处理异常,并确保总是会调用 alwaysback。然后无论哪种选择都不如语言所提供的 finally 简单和优雅。

总结

在异步编程中使用回调函数改变了传统的编程模型,并且引发了下面的问题:

  1. 我们再也不能使用简单的调用-返回编程模型。
  2. 我们再也不能使用 try/catch/finally 来处理异常。
  3. 我们必须为可能执行异步操作的每个函数的签名增加 callback 和 errback 参数。

事实上我们可以做得更好。在 JavaScript 中,还有另一种异步编程模型,更接近于标准的调用-返回模型,非常类似于 try/catch/finally,并且不会强迫我们为大量的函数增加两个回调函数参数。

下一步,我们将看看 Promises,以及它们如何帮助异步编程回归到更简单、更友好的模型。


原文:Async Programming is Messy



blog comments powered by Disqus