繁体   English   中英

异常处理,抛出错误,在承诺内

[英]exception handling, thrown errors, within promises

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

现在这就是我遇到麻烦的地方。

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

但是,由于所有代码都包含在 Promise 中,因此这些抛出的异常实际上是作为失败的 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 ...
});

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

从我读到的,这是一个功能,而不是一个问题,有承诺。 但是,我无法理解为什么您总是希望完全相同地对待异常和预期拒绝。

一种情况是关于代码中的实际错误,可能无法恢复——另一种情况可能是缺少配置信息、参数或可恢复的东西。

感谢您的帮助!

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

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

域不能解决问题,因为它们不能确保资源不泄漏

在问题#5114#5149 中阅读更多内容。

现在您可以尝试对此“聪明”,并根据一定数量的错误制定某种流程回收策略,但无论您采用何种策略,它都会严重改变节点的可扩展性配置文件。 我们说的是每个进程每秒有几十个请求,而不是几千个。

然而,promise 捕获所有异常,然后以与同步异常向上传播堆栈的方式非常相似的方式传播它们。 此外,他们经常提供一种方法, finally其目的是要成为一个相当于try...finally由于这两个特点,我们可以通过建立“上下文经理人”(类似于封装了清理逻辑with蟒蛇usingC# 中或在Java 中try-with-resources ) 总是清理资源。

让我们假设我们的资源被表示为具有acquiredispose方法的对象,这两个方法都返回承诺。 调用函数时没有建立连接,我们只返回一个资源对象。 稍后将using对象处理此对象:

function connect(url) {
  return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}

我们希望 API 像这样工作:

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

我们可以轻松实现这个 API:

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

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

但是,如果处理资源确实失败,那确实是终止的好理由。 在这种情况下,我们极有可能泄露了资源,最好开始结束该过程。 但是现在我们崩溃的机会被隔离在我们代码的一小部分 - 实际上处理可泄漏资源的部分!

注意:terminate 基本上是在带外抛出,因此 promise 无法捕获它,例如process.nextTick(() => { throw e }); . 什么实现有意义可能取决于您的设置——基于 nextTick 的实现类似于回调保释的方式。

如何使用基于回调的库? 它们可能不安全。 让我们看一个例子,看看这些错误可能来自哪里以及哪些可能导致问题:

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

mayThrowError2()是内回调中,将仍然崩溃的过程中,如果它抛出,即使unwrapped的又一承诺的范围内叫.then 这些类型的错误不会被典型的promisify包装器捕获,并且会像往常一样继续导致进程崩溃。

然而, mayThrowError1()将由如果调用中的承诺被捕获.then ,和内部分配的资源可能会泄露。

我们可以编写一个偏执版本的promisify ,以确保任何抛出的错误都是不可恢复的,并使进程崩溃:

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

使用其他承诺中的promisified函数.then回调现在,如果展开抛出,落回重磨式碰撞模式进程崩溃的结果。

人们普遍希望随着您使用越来越多的基于 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的回调收到错误时,代码会忘记处理资源。

上下文管理器不会发生这种问题。 您不能忘记调用 dispose:您不必这样做,因为using为您服务!

参考资料: 为什么我要切换到 promises上下文管理器和事务

这几乎是 Promise 最重要的特性。 如果它不存在,您不妨使用回调:

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

然而,上面的代码可以用 promise 更清楚地表达,因为只有一个异常通道而不是 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)糖。 如果您了解异常流的工作原理,您会发现通常使用.then(fnSuccess, fnFail)是一种反模式

重点不是.then(success, fail) over , 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");
}

(同步代码实际上会更丑,因为 javascript 没有输入捕获)

Promise 拒绝只是失败抽象的一种形式。 节点式回调(err、res)和异常也是如此。 由于 Promise 是异步的,因此您不能使用 try-catch 来实际捕获任何内容,因为错误可能不会发生在事件循环的同一个滴答中。

一个简单的例子:

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,所以你使用拒绝来处理错误。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM