承诺中的异常处理,抛出错误

时间:2013-10-22 21:27:57

标签: javascript node.js promise

我正在运行外部代码作为node.js服务的第三方扩展。 API方法返回promise。已解决的承诺意味着该行动已成功执行,失败的承诺意味着执行该行动存在一些问题。

现在,我遇到了麻烦。

由于第三方代码未知,可能存在错误,语法错误,类型问题,任何可能导致node.js抛出异常的事情。

但是,由于所有代码都包含在promises中,因此这些抛出的异常实际上会作为失败的承诺返回。

我试图将函数调用放在try / catch块中,但它从未被触发:

// worker process
var mod = require('./3rdparty/module.js');
try {
  mod.run().then(function (data) {
    sendToClient(true, data);
  }, function (err) {
    sendToClient(false, err);
  });
} catch (e) {
  // unrecoverable error inside of module
  // ... send signal to restart this worker process ...
});

在上面的伪代码示例中,当抛出错误时,它会在失败的promise函数中出现,而不是在catch中。

从我读到的内容来看,这是承诺的一个特征,而不是问题。然而,我无法解决为什么你总是想要处理异常和预期的拒绝完全一样。

一个案例是关于代码中的实际错误,可能无法恢复 - 另一个案例可能是缺少配置信息,参数或可恢复的东西。

感谢您的帮助!

3 个答案:

答案 0 :(得分:13)

崩溃并重新启动进程不是处理错误的有效策略,甚至不是错误。在Erlang中会很好,这个过程很便宜并且可以做一个孤立的事情,比如服务一个客户端。这不适用于节点,其中流程的成本要高出几个数量级,并且一次为数千个客户提供服务

假设您的服务每秒有200个请求。如果有1%的人在你的代码中遇到了投掷路径,那么每秒会有20个进程关闭,大约每50毫秒一次。如果你有4个内核,每个内核有1个进程,那么你将在200ms内丢失它们。因此,如果一个进程需要超过200毫秒才能启动并准备服务请求(对于不加载任何模块的节点进程,最低成本约为50毫秒),我们现在已成功拒绝服务。更不用说用户遇到错误往往会做出像反复刷新页面,从而加剧了问题。

域名无法解决问题,因为他们cannot ensure that resources are not leaked

详细了解问题#5114#5149

然而,promises会捕获所有异常,然后以与同步异常如何在堆栈中传播的方式非常类似的方式传播它们。此外,他们经常提供方法finally,相当于try...finally感谢这两个功能,我们可以构建“上下文管理器”(如在python中)清理资源:

function using(resource, fn) {
  // wraps it in case the resource was not promise
  var pResource = Promise.cast(resource); 
  return pResource.then(fn).finally(function() { 
    return pResource.then(function(resource) { 
      return resource.dispose(); 
    }); 
  });
}

然后像这样使用它们:

function connectAndFetchSomething(...) {
  return using(client.connect(host), function(conn) {
    var stuff = JSON.parse(something);
    return conn.doThings(stuff).then(function(res) { 
      return conn.doOherThingWith(JSON.parse(res)); 
    ));
  }); 
});

在使用fn参数内返回的promise链完成后,将始终处理资源。即使在该函数内抛出错误(例如来自JSON.parse)或其内部.then闭包(如第二个{​​{1}}),或者链中的承诺被拒绝(相当于回调调用错误)。这就是为什么它对承诺捕获错误并传播它们非常重要。

编辑:但是我们如何处理遵循throw-crash范式的库?我们不能确保他们已经清理了他们的资源 - 我们怎样才能避免承诺颠覆他们的例外?

通常这些库使用节点样式回调,我们需要用promises包装它们。例如,我们可能有:

JSON.parse

function unwrapped(arg1, arg2, done) { var resource = allocateResource(); mayThrowError1(); resource.doesntThrow(arg1, function(err, res) { mayThrowError2(arg2); done(err, res); }); } 在内部回调中,即使在另一个承诺mayThrowError2()

中调用unwrapped,它仍然会在进程失败时崩溃

但是,如果在.then内调用,mayThrowError1()将被承诺捕获,并且内部分配的资源将泄漏。

我们可以以确保任何抛出的错误都无法恢复并使进程崩溃的方式包装此函数:

.then

在另一个promise的function wrapped(arg1, arg2) { var defer = Promise.pending(); try { unwrapped(arg1, arg2, function callback(err, res) { if (err) defer.reject(err); else defer.fulfill(res); }); } catch (e) { process.nextTick(function rethrow() { throw e; }); } } 回调中使用包装函数现在会导致进程崩溃,如果解包,则回退到throw-crash范例。

一般希望当你使用越来越多的基于promise的库时,他们会使用上下文管理器模式来管理他们的资源,因此你不需要让进程崩溃。

这些解决方案都不是防弹的 - 甚至不会因抛出的错误而崩溃。尽管没有投掷,但很容易意外地编写泄漏资源的代码。例如,即使没有抛出资源,此节点样式函数也会泄漏资源:

.then

为什么呢?因为当function unwrapped(arg1, arg2, done) { var resource = allocateResource(); resource.doSomething(arg1, function(err, res) { if (err) return done(err); resource.doSomethingElse(res, function(err, res) { resource.dispose(); done(err, res); }); }); } 的回调收到错误时,代码会忘记处理资源。

上下文管理器不会发生这种问题。您不能忘记致电处置:您不必,因为doSomething为您做了!

参考文献:why I am switching to promisescontext managers and transactions

答案 1 :(得分:2)

这几乎是承诺最重要的特征。如果它不在那里,你也可以使用回调:

var fs = require("fs");

fs.readFile("myfile.json", function(err, contents) {
    if( err ) {
        console.error("Cannot read file");
    }
    else {
        try {
            var result = JSON.parse(contents);
            console.log(result);
        }
        catch(e) {
            console.error("Invalid json");
        }
    }

});

(在你说JSON.parse是js中唯一抛出的东西之前,你是否知道即使将变量强制转换为数字+a也可以抛出TypeError

但是,使用promises可以更清楚地表达上述代码,因为只有一个异常通道而不是2:

var Promise = require("bluebird");
var readFile = Promise.promisify(require("fs").readFile);

readFile("myfile.json").then(JSON.parse).then(function(result){
    console.log(result);
}).catch(SyntaxError, function(e){
    console.error("Invalid json");
}).catch(function(e){
    console.error("Cannot read file");
});

请注意,catch.then(null, fn)的糖。如果您了解异常流程的工作原理,您会发现它有点anti-pattern to generally use .then(fnSuccess, fnFail)

关于.then(success, fail) , function(fail, success)超过try { var result = JSON.parse(readFileSync("myjson.json")); console.log(result); } catch(SyntaxError e) { console.error("Invalid json"); } catch(Error e) { console.error("Cannot read file"); } (IE不是替代附加回调的替代方法),但完全没有,但是编写的代码看起来几乎相同正如在编写同步代码时所看到的那样:

{{1}}

(同步代码在实际中实际上会更丑陋,因为javascript没有打字捕获)

答案 2 :(得分:1)

承诺拒绝只是来自失败抽象。节点样式的回调(错误,res)和异常也是如此。由于promises是异步的,你不能使用try-catch来实际捕获任何东西,因为错误可能不会发生在事件循环的同一个tick中。

一个简单的例子:

function test(callback){
    throw 'error';
    callback(null);
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

这里我们可以捕获错误,因为函数是同步的(虽然基于回调)。另:

function test(callback){
    process.nextTick(function () {
        throw 'error';
        callback(null); 
    });
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

现在我们无法捕捉错误!唯一的选择是在回调中传递它:

function test(callback){
    process.nextTick(function () {
        callback('error', null); 
    });
}

test(function (err, res) {
    if (err) return console.log('Caught: ' + err);
});

现在它的工作方式与第一个示例相同。这同样适用于承诺:您不能使用try-catch,因此您可以使用拒绝来进行错误处理。