首页 文章

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

提问于
浏览
19

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

现在这里我遇到了麻烦 .

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

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

我试图将函数调用放在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 回答

  • 13

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

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

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

    阅读更多问题#5114#5149

    但是,promise会捕获所有异常,然后以非常类似于同步异常如何在堆栈中传播的方式传播它们 . 另外,它们经常提供一个方法 finally ,这意味着相当于 try...finally 由于这两个特性,我们可以构建始终清理资源的"context-managers"(如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 闭包(如第二个 JSON.parse ),或者链中的promise被拒绝(相当于回调调用错误) . 这就是为什么它对于承诺捕获错误并传播它们如此重要 .

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

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

    function unwrapped(arg1, arg2, done) {
      var resource = allocateResource();
      mayThrowError1();
      resource.doesntThrow(arg1, function(err, res) {
        mayThrowError2(arg2);
        done(err, res);
      });
    }
    

    mayThrowError2() 在内部回调中,如果它抛出,仍会使进程崩溃,即使 unwrapped 在另一个promise中被调用 .then

    但是,如果在 .then 内调用, mayThrowError1() 将被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; });
      }
    }
    

    在另一个promise的 .then 回调中使用包装函数现在会导致进程崩溃,如果解包抛出,则回退到throw-crash范例 .

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

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

    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 的回调收到错误时,代码会忘记处理资源 .

    这样的问题不是必须的,因为 using 为你做了!

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

  • 1

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

    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) .

    The point is not at all.then(success, fail) over , function(fail, success) (I.E . 它不是附加回调的替代方法)但是使编写的代码看起来与编写同步代码时看起来几乎相同:

    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");
    }
    

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

  • 2

    承诺拒绝只是来自失败抽象 . 节点样式的回调(错误,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);
    });
    

    现在它就像第一个例子一样工作 . 同样适用于promises:你不能使用try-catch,所以你使用拒绝来进行错误处理 .

相关问题