异常处理,抛出错误,在承诺内 [英] exception handling, thrown errors, within promises

查看:48
本文介绍了异常处理,抛出错误,在承诺内的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am running external code as a 3rd party extension to a node.js service. The API methods return promises. A resolved promise means the action was carried out successfully, a failed promise means there was some problem carrying out the operation.

现在我遇到了麻烦.

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

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.

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

However, since all the code is wrapped up in promises, these thrown exceptions are actually coming back as failed promises.

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

I tried to put the function call within a try/catch block, but it's never triggered:

// 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 中.

In the above psuedo-code example, when an error is thrown it turns up in the failed promise function, and not in the 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.

感谢您的帮助!

推荐答案

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

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. That doesn't apply in node, where a process costs orders of magnitude more and serves thousands of clients at once

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

Lets say that you have 200 requests per second being served by your service. If 1% of those hit a throwing path in your code, you would get 20 process shutdowns per second, roughly one every 50ms. If you have 4 cores with 1 process per core, you would lose them in 200ms. 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. Not to mention that users hitting an error tend to do things like e.g. repeatedly refresh the page, thereby compounding the problem.

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

Domains don't solve the issue because they cannot ensure that resources are not leaked.

在问题 #5114#5149.

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

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.

然而,promise 捕获所有异常,然后以与同步异常向上传播堆栈的方式非常相似的方式传播它们.此外,他们经常提供一个方法 finally 相当于 try...finally 由于这两个特性,我们可以通过以下方式封装清理逻辑构建上下文管理器"(类似于python<中的with/a>, usingC#try-with-resourcesJava) 总是清理资源.

However, promises catch all exceptions and then propagate them in a manner very similar to how synchronous exceptions propagate up the stack. 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.

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

Lets assume our resources are represented as objects with acquire and dispose methods, both of which return promises. 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:

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

我们希望 API 像这样工作:

We want the API to work like this:

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

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

We can easily achieve this 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),或者如果链中的承诺被拒绝(相当于回调调用有错误).这就是 Promise 捕获错误并传播它们如此重要的原因.

The resources will always be disposed of after the promise chain returned within using's fn argument completes. Even if an error was thrown within that function (e.g. 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). 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!

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

Note: terminate is basically throwing out-of-band so that promises cannot catch it, e.g. process.nextTick(() => { throw e });. What implementation makes sense might depend on your setup - a nextTick based one works similar to how callbacks bail.

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

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() 位于内部回调中,如果抛出,即使 unwrapped 在另一个 promise 的 .then.这些类型的错误不会被典型的 promisify 包装器捕获,并且会像往常一样继续导致进程崩溃.

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. These kinds of errors aren't caught by typical promisify wrappers and will continue to cause a process crash as per usual.

然而,mayThrowError1().then中调用会被promise捕获,内部分配的资源可能会泄漏.

However, mayThrowError1() will be caught by the promise if called within .then, and the inner allocated resource might leak.

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

We can write a paranoid version of promisify that makes sure that any thrown errors are unrecoverable and crash the process:

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

在另一个 promise 的 .then 回调中使用 promisified 函数现在会导致进程崩溃,如果未包装的 throws,退回到 throw-crash 范式.

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.

人们普遍希望,随着您使用越来越多的基于 Promise 的库,他们将使用上下文管理器模式来管理其资源,因此您无需让进程崩溃.

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.

这些解决方案都不是万无一失的 - 甚至不会因抛出的错误而崩溃.很容易不小心写出尽管没有抛出却泄漏资源的代码.例如,这个节点样式函数即使不抛出也会泄漏资源:

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

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

Why? Because when doSomething's callback receives an error, the code forgets to dispose of the resource.

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

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!

参考:为什么我要改用 promises, 上下文管理器和事务

这篇关于异常处理,抛出错误,在承诺内的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆