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

Nic*_*ngs 19 javascript node.js promise

我正在运行外部代码作为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 ...
});
Run Code Online (Sandbox Code Playgroud)

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

从我读到的,这是一个功能,而不是一个问题,与承诺.然而,我无法解决为什么你总是想要处理异常和预期的拒绝完全一样.

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

谢谢你的帮助!

Gor*_*sev 13

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

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

域无法解决问题,因为它们无法确保资源不会泄露.

阅读更多问题#5114#5149

但是,promise会捕获所有异常,然后以非常类似于同步异常如何在堆栈中传播的方式传播它们.另外,他们经常提供一种方法finally,这意味着相当于try...finally这两种功能,我们可以构建"上下文管理器"(如python),它总是清理资源:

function connect(url) {
  return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}
Run Code Online (Sandbox Code Playgroud)

然后像这样使用它们:

using(connect(process.env.DATABASE_URL), async (conn) => {
  await conn.query(...);
  do other things
  return some result;
});
Run Code Online (Sandbox Code Playgroud)

在using with参数内返回的promise链完成后,将始终处理资源.即使在该函数(例如from using)或其内部acquire闭包(如第二个dispose)中抛出错误,或者链中的promise被拒绝(相当于回调调用错误).这就是为什么它对于承诺捕获错误并传播它们如此重要.

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

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

function using(resource, fn) {
  return Promise.resolve()
    .then(() => resource.acquire())
    .then(item => 
      Promise.resolve(item).then(fn).finally(() => 
        // bail if disposing fails, for any reason (sync or async)
        Promise.resolve()
          .then(() => resource.dispose(item))
          .catch(terminate)
      )
    );
}
Run Code Online (Sandbox Code Playgroud)

using在内部回调中,如果它抛出,仍然会使进程崩溃,即使fn在另一个promise中被调用JSON.parse

但是,.then如果在JSON.parse内部调用,将被promise所捕获,并且内部分配的资源将泄漏.

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

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  mayThrowError1();
  resource.doesntThrow(arg1, (err, res) => {
    mayThrowError2(arg2);
    done(err, res);
  });
}
Run Code Online (Sandbox Code Playgroud)

在另一个promise的process.nextTick(() => { throw e });回调中使用包装函数现在会导致进程崩溃,如果解包抛出,则回退到throw-crash范例.

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

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

function paranoidPromisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) =>   
      try {
        fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
      } catch (e) {
        process.nextTick(() => { throw e; });
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

为什么?因为当mayThrowError2()回调收到错误时,代码会忘记处理资源.

上下文管理器不会发生这种问题.您不能忘记致电处置:您没有必要,因为unwrapped它适合您!

参考文献:为什么我要转向承诺,上下文管理器和交易