穹顶灯打不出阴暗面_承诺的阴暗面

穹顶灯打不出阴暗面

Since the release of es6 many new features have found their way into NodeJS, but non had quite the same impact as promises. Promises have been developed for the browser before es6 was even a thing. There were several implementations that have been used like jQuery's deferred object before the standard made them obsolete. Promises were quite useful on the client especially if you had to make a lot of async calls, or if your API was a complete mess and you had to gather your async calls from all over the place. For me the later was usually the case or at least that was when I've found promises the most useful. The ability to pass around any promise and attach as many callbacks to it as well as chaining them as many times as you wanted made promises highly versatile, but that was for the client. The server is different. On the server you need to make an insane amount of async calls compared to the client. The client normally only needed to call your api server asynchronously, but the server needs to talk to the database, the file system, external APIs like payment and communication and any core service you might have to use. Essentially: a lot of stuff. Any issues we might have on the client due to promises will be amplified on the server due to the higher rate of usage and increased chance to make mistakes.

自es6发行以来,NodeJS已发现了许多新功能,但它们的功能却与承诺一样。 在es6尚未成为现实之前就已经为浏览器开发了承诺。 在标准使它们过时之前,已经使用了几种实现方式,例如jQuery的延迟对象。 承诺在客户端上非常有用,尤其是当您必须进行大量异步调用,或者您的API完全混乱并且必须从各地收集异步调用时。 对我而言,通常情况较晚,或者至少在我发现诺言最有用的时候。 传递任何诺言并附加尽可能多的回调以及将其链接到所需次数的能力使诺言具有高度的通用性,但这是针对客户的。 服务器不同。 与客户端相比,在服务器上您需要进行大量的异步调用。 客户端通常只需要异步调用api服务器,但是服务器需要与数据库,文件系统,外部API(如付款和通信)以及您可能需要使用的任何核心服务进行对话。 本质上:很多东西。 由于更高的使用率和增加的犯错机会,由于承诺,我们在客户端上可能遇到的任何问题都会在服务器上放大。

If we look at the code we use to make promises at first, they don't seem very different from normal functions, but there is one key feature that makes them unique. Promises catches all exceptions that are raised inside them synchronously. This, while very useful in most cases, can cause some issues if you are not prepared to handle them. When an exception is thrown the promise gets rejected and will call its rejected callback, if there's any. But what happens, if we don't handle the rejected state of the promise? It depends on the NodeJS version but generally a warning will be printed out and the function that raised the exception will exit. Rejecting promises via throwing exceptions is something that were often used in the old browser days of promise libraries and is considered normal, but is it actually a good thing. It is good or at least okay if you actually want to reject a promise, however what if you throw an error not because you wanted but because you made a mistake? In that case you need to find the bug and fix it and it is in that specific case when letting a exception crash your server and print out a stack trace would be really useful. So what do we get instead of that? In NodeJS 6 and 7 we will get a UnhandledPromiseRejectionWarning which in most cases will tell you what caused the error, but not where. In node 8 we will also get a short stack trace as well. So upgrading to node 8 could potentially resolve our issues, so as long as you can do that you might think that's all we have to do to solve this issue. Unfortunately, node 8 is not yet used by most companies and makes up less than 10% of the market.

如果我们先看一下用来做承诺的代码,它们看上去与普通函数没有太大区别,但是有一个关键特性使它们独一无二。 Promise同步捕获它们内部引发的所有异常。 尽管在大多数情况下这很有用,但如果您不准备处理这些问题,则可能导致某些问题。 引发异常时,promise将被拒绝,并将调用被拒绝的回调(如果存在)。 但是,如果我们不处理承诺的被拒状态,会发生什么? 它取决于NodeJS版本,但通常会打印出警告,并退出引发异常的函数。 通过抛出异常来拒绝承诺是在古老的承诺库浏览器中经常使用的东西,被认为是正常的,但这实际上是一件好事。 如果您实际上想拒绝承诺,那很好或至少可以,但是,如果您不是因为想要而是因为犯错而抛出错误怎么办? 在那种情况下,您需要查找并修复该错误,并且在这种特定情况下,当让异常使服务器崩溃并打印出堆栈跟踪时,这将非常有用。 那么,我们能得到什么呢? 在NodeJS 6和7中,我们将获得UnhandledPromiseRejectionWarning,在大多数情况下,它将告诉您导致错误的原因,而不是原因。 在节点8中,我们还将获得简短的堆栈跟踪。 因此,升级到节点8可能会解决我们的问题,因此,只要您能够做到,您可能会认为这是我们解决该问题所要做的。 不幸的是,大多数公司尚未使用节点8,而节点8的市场份额不到10%。

Since node 7 a promise rejection warning will also give you another warning:

由于节点7出现了承诺拒绝警告,因此还会给您另一个警告:

"DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code."

“ DeprecationWarning:不提倡处理未处理的诺言拒绝。将来,未处理的诺言拒绝将以非零退出代码终止Node.js进程。”

Note that this warning doesn't say that it will raise an exception, but that it will crash your server no matter what. That's quite harsh, don't you think? This change would definitely break some code if it were implemented today. Interest in UnhandledPromiseRejectionWarning has risen in conjunction of the popularity and use of promises. We can even measure how much using google trends.

请注意,此警告并不是说它将引发异常,但是无论如何它将使您的服务器崩溃。 那很苛刻,你不觉得吗? 如果今天实施,此更改肯定会破坏某些代码。 随着承诺的普及和使用,人们对UnhandledPromiseRejectionWarning的兴趣有所增加。 我们甚至可以使用Google趋势来衡量多少。

The people who had searched for this particular warning have increased significantly since native promises and this warning were introduced to node. During 2017 the number of searches doubled which also probably means that the number of people using promises in NodeJS has also doubled. Perhaps this is the reason the node team wants to completely purge the warning from its stack.

自从将本地承诺和此警告引入节点以来,搜索此特定警告的人员已大大增加。 在2017年期间,搜索数量翻了一番,这也可能意味着NodeJS中使用诺言的人数也翻了一番。 也许这就是节点团队想要从其堆栈中完全清除警告的原因。

It is understandable that in case a promise rejection is not handled it is better to crash the server than just issuing a warning. Imagine what would happen to an API route if a rejection was not handled. In that cases the response would not be sent to the client, since the function would exit before it reached that point, but it would also not close the socket since the server would not crash, and it would just wait there until it gets timeout after two minutes. If several such requests were made to the server in the span of two minutes we could run out of sockets very quickly which would block our service for good. If on the other hand we crash and restart, we should be able to serve some requests for a little while at least. Clearly neither case is desirable, so we should put a catch rejection handler to the end of every promise chain we create. This would prevent the server from crashing or raising a warning which would also allow us to reply to API requests in some fashion. The problem with the catch method is that it is only a glorified reject callback no different from the ones supplied via the second parameter of the then promise method.

可以理解,如果未处理承诺拒绝,则使服务器崩溃比仅发出警告更好。 想象一下,如果不处理拒绝,API路由会发生什么。 在那种情况下,响应不会被发送到客户端,因为该函数会在到达该点之前退出,但由于服务器不会崩溃,它也不会关闭套接字,它只会在那里等待直到超时后超时。 2分钟。 如果在两分钟内对服务器发出了几次这样的请求,我们可能很快就会用光套接字,这将永远阻塞我们的服务。 另一方面,如果我们崩溃并重新启动,则我们至少应该能够处理一些请求。 显然,这两种情况都不是理想的,因此我们应该将catch拒绝处理程序放置在我们创建的每个promise链的末尾。 这样可以防止服务器崩溃或发出警告,这也允许我们以某种方式回复API请求。 catch方法的问题在于,它只是美化的拒绝回调,与通过then promise方法的第二个参数提供的拒绝回调没有区别。

The biggest issue that I have with promises is that all exceptions are caught by the rejection handler regardless of the reason they were raised. It is normal to except that async calls may fail and it is normal to handle that possibility but catching all exceptions will also catch the errors in your code as well. When normally the system would crash and give you a stack trace with promises the code will try to handle the exception and possibly fail that async call silently letting the rest of your code run uninterrupted. It is very difficult to differentiate promise rejection that was thrown by the system and an exception thrown by the code, and even if you could that it would just be over engineering. The only way to handle promises properly is to write a massive number of tests, but the fact that you simply must do that is not a positive feature in of itself. Not everyone does that and not everyone is allowed to, and there's no good reason to make things difficult for them.

我所承诺的最大问题是,所有异常均由拒绝处理程序捕获,无论引发异常的原因如何。 除了异步调用可能会失败,这是正常的,并且处理这种可能性是正常的,但是捕获所有异常也会捕获代码中的错误。 通常情况下,系统崩溃并为您提供带有Promise的堆栈跟踪,代码将尝试处理异常,并可能使异步调用静默失败,从而使其余代码不间断运行。 区分由系统引发的承诺拒绝和由代码引发的异常是非常困难的,即使您可以做到,这仅仅是工程上的问题。 正确处理承诺的唯一方法是编写大量测试,但是您必须要做的事实本身并不是一个积极的功能。 不是每个人都这样做,也不是每个人都被允许,而且没有充分的理由使他们感到困难。

Exceptions raised in any Async call cannot be caught by a try catch block so it makes sense to catch them if necessary. The keyword here is "necessary". It is not necessary to catch them during development just as expressJS will not catch them except in production, but even if the later catches them it will at the very least stop the code execution for that particular call, which you cannot do for promises. The proper way to handle exceptions in promises or for any other async calls is (a) to provide them with an exception handler, which if provided will be executed if an exception is thrown and (b) stop the promise chain or the rest of the code from executing. This handler can be propagated down the promise chain and if not set will allow the exception to bubble up and crash the server.

任何异步调用中引发的异常都不能由try catch块捕获,因此在必要时捕获它们是有意义的。 此处的关键字为“必需”。 不必在开发过程中捕获它们,就像expressJS在生产环境中不会捕获它们一样,但是即使稍后捕获它们,它也至少会停止该特定调用的代码执行,而您不能为promise做这些。 处理Promise或其他任何异步调用中异常的正确方法是(a)向它们提供异常处理程序,如果抛出异常,则将执行该异常处理程序;并且(b)停止Promise链或其余的执行代码。 该处理程序可以沿promise链传播,如果未设置,将允许异常冒泡并使服务器崩溃。

Some people think that throwing inside promises is necessary to invoke the reject callback, but that was never true. Even today you can just return a Promise.reject(someError) to fail any promise where you would normally do a throw. If you asked why throwing errors are used to reject promises not many could answer. I'm not sure if there is an answer to begin with other than that this was the way promises were implemented for the browser many years ago, and ECMA just reimplemented this somewhat broken standard into ES6 and Node took it from there. Was it a good idea to introduce this version of promises to the standard and to migrate it to the server side? The fact that Node is moving away from the standard should give us some doubt. It's not even true that promises are the only way to handle the dreaded callback hell. There are other solutions like the async and RQ libraries for example which include methods like parallel and waterfall that allow coders to execute async calls in a more organized manner. At least on the server side it is quite rare to need more than some combination of the methods these libraries provide. The reason why promises were introduced in the standard might have been simply because they were popular thanks to jQuery. Implementing exception handling would be easier with a traditional async library, but that doesn't mean it cannot be done with promises. Even today you could override the then method on the Promise prototype and the Promise constructor to do that.

有人认为,内部承诺必须是调用拒绝回调的必要条件,但事实并非如此。 即使在今天,您也可以返回Promise.reject(someError)以使通常无法进行throw promise失败。 如果您问为什么使用抛出错误来拒绝诺言,没有多少人可以回答。 我不确定是否有一个答案可以解决,除了这是许多年前为浏览器实现承诺的方式之外,ECMA只是将这个有点破损的标准重新实现为ES6,然后Node从那里得到了答案。 将这个版本的Promise引入标准并将其迁移到服务器端是个好主意吗? Node正在偏离标准,这一事实使我们有些怀疑。 甚至诺言是处理可怕的回调地狱的唯一方法,这甚至不是事实。 还有其他解决方案,例如asyncRQ库,其中包括诸如parallelwaterfall类的方法,这些方法允许编码人员以更有组织的方式执行异步调用。 至少在服务器端,很少需要这些库提供的方法的某种组合。 在标准中引入承诺的原因可能仅仅是因为它们由于jQuery而受欢迎。 使用传统的异步库,实现异常处理会更容易,但这并不意味着它不能用promises完成。 即使在今天,您也可以覆盖Promise原型和Promise构造函数上的then方法来执行此操作。


Promise.prototype.then = (function () {
  const then = Promise.prototype.then;
  const fixCall = function(promise, next){
    if (!next) {
      return null;
    }
    return function (val) {
      try {
        let newPromise = next.call(promise, val);
        if(newPromise){
          newPromise.error = promise.error;
        }
        return newPromise;
      } catch (exception) {
        setTimeout(function () {
          if (promise.error) {
            promise.error(exception);
          } else {
            throw(exception);
          }
        }, 0);
        return new Promise(()=>{});
      }
    }
  };
  return function (success, fail, error) {
    this.error = this.error || error;
    let promise = then.call(this, fixCall(this, success), fixCall(this, fail));
    promise.error = this.error;
    return promise;
  }
}());
function createPromise(init, error){
  let promise = new Promise(init);
  promise.error = error;
  return promise;
}  



I mentioned before that async calls cannot be caught by a try catch block and that is true even inside a promise, so it is possible to break out from a promise using a setTimeout or a setImmediate call. So, if we catch an exception we just do that unless an exception handler was provided in which case we call that instead. In both cases we want to stop the rest of the promise chain from executing and we can do that by simply returning an empty promise that never gets resolved. Obviously, this code is only here to demonstrate that it can be done, and even though now you can handle exceptions properly you haven't lost any of the original functionality.

我之前提到过,异步调用无法被try catch块捕获,即使在promise中也是如此,因此可以使用setTimeoutsetImmediate调用从promise中setImmediate 。 因此,如果我们捕获到异常,除非提供了异常处理程序,否则我们只会这么做。 在这两种情况下,我们都希望停止执行承诺链的其余部分,我们可以通过简单地返回一个永远不会解决的空承诺来做到这一点。 显然,此代码只是在这里证明它可以完成,即使现在您可以正确处理异常,也不会丢失任何原始功能。

One major problem of promises is that you might be using them without realizing it. There are some popular libraries out there that use promises behind the scenes and at the same time allow you to specify traditional callbacks but will execute them inside of the promises they use. What this means is that any exception will be caught without your knowledge or ability to add a reject handler for them, so they will raise the UnhandledPromiseRejectionWarning for now. You will certainly scratch you head if you see this warning without having a single promise in your code, the same way I did some time ago. Now normally you would get a relatively useful error message in the warning, but if you are executing the bad code inside a method of a async library, then it will probably fail in way most of us can't comprehend. Once you enter a promise all of your callbacks will be executed in the context of that promise and unless you break out of that using something like setTimeout it will take over all of your code without you realizing it. I will put here an example which uses an older version of the Monk MongoDB module. This bug has been fixed but you can never know if another library will do something similar. So, knowing that monk uses promises, what do you believe will happen if I execute this code on an empty database?

许诺的一个主要问题是您可能在没有兑现的情况下使用它们。 有一些流行的库在后台使用promise,同时允许您指定传统的回调,但是将在它们使用的promise中执行它们。 这意味着在您不了解或没有能力为其添加reject处理程序的情况下,将捕获任何异常,因此它们现在将引发UnhandledPromiseRejectionWarning。 如果看到此警告而您的代码中没有一个承诺,您肯定会不知所措,就像我前一段时间所做的一样。 现在通常您会在警告中收到一条相对有用的错误消息,但是如果您在异步库的方法中执行错误的代码,则它可能会以我们大多数人无法理解的方式失败。 输入承诺后,所有回调将在该承诺的上下文中执行,除非您使用setTimeout之类的方法突破了该要求,否则它将接管所有代码,而无需您意识到。 我将在这里放一个示例,该示例使用Monk MongoDB模块的较旧版本。 该错误已得到修复,但是您永远无法知道另一个库是否会执行类似的操作。 因此,知道僧侣使用了Promise,如果我在一个空数据库上执行此代码,您相信会发生什么?


async.parallel({
  value: cb => collection.find({}, cb)
}, function (err, result) {
  console.log(result.test.test); //this line throws an exception because result is an empty object
});



The answer is:

答案是:

(node:29332) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Callback was already called.

Unless you are using Node 8, in which case you will get:

除非使用的是节点8,否则将获得:


(node:46955) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:46955) UnhandledPromiseRejectionWarning: Error: Callback was already called.
    at /node_modules/async/dist/async.js:955:32
    at /node_modules/async/dist/async.js:3871:13
    at /node_modules/monk-middleware-handle-callback/index.js:13:7
    at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)


Good luck finding the cause of that 😊.

祝你好运。

资料来源: (Sources:)

  1. https://semaphoreci.com/blog/2017/11/22/nodejs-versions-used-in-commercial-projects-in-2017.html

    https://semaphoreci.com/blog/2017/11/22/nodejs-versions-used-in-commercial-projects-in-2017.html

  2. https://trends.google.com/trends/explore?date=2016-03-30%202018-03-30&q=UnhandledPromiseRejectionWarning

    https://trends.google.com/trends/explore?date=2016-03-30%202018-03-30&q=UnhandledPromiseRejection警告

  3. https://github.com/nekdolan/promise-tests

    https://github.com/nekdolan/promise-tests

翻译自: https://davidwalsh.name/dark-side-promises

穹顶灯打不出阴暗面

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值