简体   繁体   English

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

[英]exception handling, thrown errors, within promises

I am running external code as a 3rd party extension to a node.js service.我正在运行外部代码作为 node.js 服务的第 3 方扩展。 The API methods return promises. API 方法返回承诺。 A resolved promise means the action was carried out successfully, a failed promise means there was some problem carrying out the operation.已解决的承诺意味着操作成功执行,失败的承诺意味着执行操作存在问题。

Now here's where I'm having trouble.现在这就是我遇到麻烦的地方。

Since the 3rd party code is unknown, there could be bugs, syntax errors, type issues, any number of things that could cause node.js to throw an exception.由于第 3 方代码未知,因此可能存在错误、语法错误、类型问题以及任何可能导致 node.js 抛出异常的事情。

However, since all the code is wrapped up in promises, these thrown exceptions are actually coming back as failed promises.但是,由于所有代码都包含在 Promise 中,因此这些抛出的异常实际上是作为失败的 Promise 返回的。

I tried to put the function call within a try/catch block, but it's never triggered:我试图将函数调用放在 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 ...
});

In the above psuedo-code example, when an error is thrown it turns up in the failed promise function, and not in the catch.在上面的伪代码示例中,当抛出错误时,它会出现在失败的承诺函数中,而不是在 catch 中。

From what I read, this is a feature, not an issue, with promises.从我读到的,这是一个功能,而不是一个问题,有承诺。 However I'm having trouble wrapping my head around why you'd always want to treat exceptions and expected rejections exactly the same.但是,我无法理解为什么您总是希望完全相同地对待异常和预期拒绝。

One case is about actual bugs in the code, possibly irrecoverable -- the other is just possible missing configuration information, or a parameter, or something recoverable.一种情况是关于代码中的实际错误,可能无法恢复——另一种情况可能是缺少配置信息、参数或可恢复的东西。

Thanks for any help!感谢您的帮助!

Crashing and restarting a process is not a valid strategy to deal with errors, not even bugs.崩溃和重新启动进程不是处理错误的有效策略,甚至不是错误。 It would be fine in Erlang, where a process is cheap and does one isolated thing, like serving a single client.在 Erlang 中会很好,在那里一个进程很便宜并且只做一件孤立的事情,比如为单个客户端提供服务。 That doesn't apply in node, where a process costs orders of magnitude more and serves thousands of clients at once这不适用于节点,其中一个流程的成本要高出几个数量级并同时为数千个客户提供服务

Lets say that you have 200 requests per second being served by your service.假设您的服务每秒处理 200 个请求。 If 1% of those hit a throwing path in your code, you would get 20 process shutdowns per second, roughly one every 50ms.如果其中有 1% 的人在您的代码中遇到了问题,那么您将每秒关闭 20 次进程,大约每 50 毫秒一次。 If you have 4 cores with 1 process per core, you would lose them in 200ms.如果您有 4 个内核,每个内核有 1 个进程,那么您将在 200 毫秒内丢失它们。 So if a process takes more than 200ms to start and prepare to serve requests (minimum cost is around 50ms for a node process that doesn't load any modules), we now have a successful total denial of service.因此,如果一个进程需要超过 200 毫秒的时间来启动和准备服务请求(对于不加载任何模块的节点进程,最低成本约为 50 毫秒),我们现在成功地完全拒绝服务。 Not to mention that users hitting an error tend to do things like eg repeatedly refresh the page, thereby compounding the problem.更不用说遇到错误的用户往往会做一些事情,例如反复刷新页面,从而使问题复杂化。

Domains don't solve the issue because they cannot ensure that resources are not leaked .域不能解决问题,因为它们不能确保资源不泄漏

Read more at issues #5114 and #5149 .在问题#5114#5149 中阅读更多内容。

Now you can try to be "smart" about this and have a process recycling policy of some sort based on a certain number of errors, but whatever strategy you approach it will severely change the scalability profile of node.现在您可以尝试对此“聪明”,并根据一定数量的错误制定某种流程回收策略,但无论您采用何种策略,它都会严重改变节点的可扩展性配置文件。 We're talking several dozen requests per second per process, instead of several thousands.我们说的是每个进程每秒有几十个请求,而不是几千个。

However, promises catch all exceptions and then propagate them in a manner very similar to how synchronous exceptions propagate up the stack.然而,promise 捕获所有异常,然后以与同步异常向上传播堆栈的方式非常相似的方式传播它们。 Additionally, they often provide a method finally which is meant to be an equivalent of try...finally Thanks to those two features, we can encapsulate that clean-up logic by building "context-managers" (similar to with in python , using in C# or try-with-resources in Java ) that always clean up resources.此外,他们经常提供一种方法, finally其目的是要成为一个相当于try...finally由于这两个特点,我们可以通过建立“上下文经理人”(类似于封装了清理逻辑with蟒蛇usingC# 中或在Java 中try-with-resources ) 总是清理资源。

Lets assume our resources are represented as objects with acquire and dispose methods, both of which return promises.让我们假设我们的资源被表示为具有acquiredispose方法的对象,这两个方法都返回承诺。 No connections are being made when the function is called, we only return a resource object.调用函数时没有建立连接,我们只返回一个资源对象。 This object will be handled by using later on:稍后将using对象处理此对象:

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

We want the API to work like this:我们希望 API 像这样工作:

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

We can easily achieve this API:我们可以轻松实现这个 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)
      )
    );
}

The resources will always be disposed of after the promise chain returned within using's fn argument completes.在 using 的fn参数中返回的承诺链完成后,资源将始终被处理。 Even if an error was thrown within that function (eg from JSON.parse ) or its inner .then closures (like the second JSON.parse ), or if a promise in the chain was rejected (equivalent to callbacks calling with an error).即使在该函数(例如从JSON.parse )或其内部.then闭包(如第二个JSON.parse )中抛出错误,或者如果链中的承诺被拒绝(相当于回调调用错误)。 This is why its so important for promises to catch errors and propagate them.这就是为什么承诺捕获错误并传播它们如此重要的原因。

If however disposing the resource really fails, that is indeed a good reason to terminate.但是,如果处理资源确实失败,那确实是终止的好理由。 Its extremely likely that we've leaked a resource in this case, and its a good idea to start winding down that process.在这种情况下,我们极有可能泄露了资源,最好开始结束该过程。 But now our chances of crashing are isolated to a much smaller part of our code - the part that actually deals with leakable resources!但是现在我们崩溃的机会被隔离在我们代码的一小部分 - 实际上处理可泄漏资源的部分!

Note: terminate is basically throwing out-of-band so that promises cannot catch it, eg process.nextTick(() => { throw e });注意:terminate 基本上是在带外抛出,因此 promise 无法捕获它,例如process.nextTick(() => { throw e }); . . What implementation makes sense might depend on your setup - a nextTick based one works similar to how callbacks bail.什么实现有意义可能取决于您的设置——基于 nextTick 的实现类似于回调保释的方式。

How about using callback based libraries?如何使用基于回调的库? They could potentially be unsafe.它们可能不安全。 Lets look at an example to see where those errors could come from and which ones could cause problems:让我们看一个例子,看看这些错误可能来自哪里以及哪些可能导致问题:

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

mayThrowError2() is within an inner callback and will still crash the process if it throws, even if unwrapped is called within another promise's .then . mayThrowError2()是内回调中,将仍然崩溃的过程中,如果它抛出,即使unwrapped的又一承诺的范围内叫.then These kinds of errors aren't caught by typical promisify wrappers and will continue to cause a process crash as per usual.这些类型的错误不会被典型的promisify包装器捕获,并且会像往常一样继续导致进程崩溃。

However, mayThrowError1() will be caught by the promise if called within .then , and the inner allocated resource might leak.然而, mayThrowError1()将由如果调用中的承诺被捕获.then ,和内部分配的资源可能会泄露。

We can write a paranoid version of promisify that makes sure that any thrown errors are unrecoverable and crash the process:我们可以编写一个偏执版本的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; });
      }
    }
  }
}

Using the promisified function within another promise's .then callback now results with a process crash if unwrapped throws, falling back to the throw-crash paradigm.使用其他承诺中的promisified函数.then回调现在,如果展开抛出,落回重磨式碰撞模式进程崩溃的结果。

Its the general hope that as you use more and more promise based libraries, they would use the context manager pattern to manage their resources and therefore you would have less need to let the process crash.人们普遍希望随着您使用越来越多的基于 Promise 的库,他们将使用上下文管理器模式来管理他们的资源,因此您不需要让进程崩溃。

None of these solutions are bulletproof - not even crashing on thrown errors.这些解决方案都不是防弹的 - 甚至不会因抛出的错误而崩溃。 Its very easy to accidentally write code that leaks resources despite not throwing.很容易不小心写出尽管没有抛出却泄漏资源的代码。 For example, this node style function will leak resources even though it doesn't throw:例如,这个节点样式函数即使没有抛出也会泄漏资源:

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

Why?为什么? Because when doSomething 's callback receives an error, the code forgets to dispose of the resource.因为当doSomething的回调收到错误时,代码会忘记处理资源。

This sort of problem doesn't happen with context-managers.上下文管理器不会发生这种问题。 You cannot forget to call dispose: you don't have to, since using does it for you!您不能忘记调用 dispose:您不必这样做,因为using为您服务!

References: why I am switching to promises , context managers and transactions参考资料: 为什么我要切换到 promises上下文管理器和事务

It is almost the most important feature of promises.这几乎是 Promise 最重要的特性。 If it wasn't there, you might as well use callbacks:如果它不存在,您不妨使用回调:

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

});

(Before you say that JSON.parse is the only thing that throws in js, did you know that even coercing a variable to a number eg +a can throw a TypeError ? (在您说JSON.parse是 js 中唯一抛出的东西之前,您是否知道即使将变量强制为数字,例如+a也会抛出TypeError

However, the above code can be expressed much more clearly with promises because there is just one exception channel instead of 2:然而,上面的代码可以用 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");
});

Note that catch is sugar for .then(null, fn) .请注意, catch.then(null, fn)糖。 If you understand how the exception flow works you will see it is kinda of an anti-pattern to generally use .then(fnSuccess, fnFail) .如果您了解异常流的工作原理,您会发现通常使用.then(fnSuccess, fnFail)是一种反模式

The point is not at all to do .then(success, fail) over , function(fail, success) (IE it is not an alternative way to attach your callbacks) but make written code look almost the same as it would look when writing synchronous code:重点不是.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");
}

(The sync code will actually be uglier in reality because javascript doesn't have typed catches) (同步代码实际上会更丑,因为 javascript 没有输入捕获)

Promise rejection is simply a from of failure abstraction. Promise 拒绝只是失败抽象的一种形式。 So are node-style callbacks (err, res) and exceptions.节点式回调(err、res)和异常也是如此。 Since promises are asynchronous you can't use try-catch to actually catch anything, because errors a likely to happen not in the same tick of event loop.由于 Promise 是异步的,因此您不能使用 try-catch 来实际捕获任何内容,因为错误可能不会发生在事件循环的同一个滴答中。

A quick example:一个简单的例子:

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

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

Here we can catch an error, as function is synchronous (though callback-based).在这里我们可以捕获错误,因为函数是同步的(虽然基于回调)。 Another:另一个:

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

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

Now we can't catch the error!现在我们无法捕捉到错误! The only option is to pass it in the callback:唯一的选择是在回调中传递它:

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

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

Now it's working just like in the first example.The same applies to promises: you can't use try-catch, so you use rejections for error-handling.现在它就像第一个例子一样工作。这同样适用于承诺:你不能使用 try-catch,所以你使用拒绝来处理错误。

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

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