【问题标题】:Skipping promise chain after handling error处理错误后跳过承诺链
【发布时间】:2014-08-12 21:43:24
【问题描述】:

使用https://github.com/kriskowal/q 库,我想知道是否可以这样做:

// Module A

function moduleA_exportedFunction() {
  return promiseReturningService().then(function(serviceResults) {
    if (serviceResults.areGood) {
      // We can continue with the rest of the promise chain
    }
    else {
      performVerySpecificErrorHandling();
      // We want to skip the rest of the promise chain
    }
  });
}

// Module B

moduleA_exportedFunction()
  .then(moduleB_function)
  .then(moduleB_anotherFunction)
  .fail(function(reason) {
    // Handle the reason in a general way which is ok for module B functions
  })
  .done()
;

基本上,如果服务结果不好,我想处理模块 A 中的故障,使用特定于模块 A 内部的逻辑,但仍然跳过承诺链中剩余的模块 B 函数。

跳过模块 B 函数的明显解决方案是从模块 A 中抛出错误/原因。但是,我需要在模块 B 中处理它。理想情况下,我希望在不需要任何额外代码的情况下这样做模块 B。

这很可能是不可能的 :) 或者违反 Q 的一些设计原则。

在这种情况下,您会建议什么样的替代方案?

我想到了两种方法,但都有各自的缺点:

  1. 从模块 A 中抛出一个特定的错误并将特定的处理代码添加到模块 B:

    .fail(function(reason) {
      if (reason is specificError) {
        performVerySpecificErrorHandling();
      }
      else {
        // Handle the reason in a general way which is ok for module B functions
      }
    })
    
  2. 在模块 A 中执行自定义错误处理,然后在处理错误后,抛出一个虚假的拒绝原因。在模块B中,添加一个忽略虚假原因的条件:

    .fail(function(reason) {
      if (reason is fakeReason) {
        // Skip handling
      }
      else {
        // Handle the reason in a general way which is ok for module B functions
      }
    })
    

解决方案 1 需要将模块 A 的特定代码添加到模块 B。

解决方案 2 解决了这个问题,但整个假拒绝方法似乎非常骇人听闻。

您能推荐其他解决方案吗?

【问题讨论】:

  • 让我们玩个小游戏——写下如果根本不涉及任何承诺,你将如何在同步代码中完成此任务?
  • 感谢您的建议:DI 意识到,在同步代码中,我可能会从模块 A 函数返回一个“shouldContinue”布尔值并在模块 B 中处理它。在基于承诺的代码中,它看起来像一个 hack ,但事后看来,它远没有假错误和诸如此类的东西那么 hacky :)
  • 不要忘记使用.spread 来返回一个数组并将其参数作为参数查看,以便能够对多个变量做出反应:Q([false,{}]).spread(function(success,data){ /* code here */})

标签: javascript node.js promise q


【解决方案1】:

我们来谈谈控制结构。

在 JavaScript 中,调用函数时代码以两种方式流动。

  • 它可以return一个值给调用者,说明它成功完成了。
  • 它可以throw给调用者一个错误,表明发生了异常操作。

它看起来像:

function doSomething(){ // every function ever
  if(somethingBad) throw new Error("Error operating");
  return value; // successful completion.
}

try{
  doSomething();
  console.log("Success");
} catch (e){
  console.log("Boo");
}

Promises 模拟了这种完全相同的行为。

在 Promises 中,当您在 .then 处理程序中调用函数时,代码会以两种方式流动:

  • 它可以return 一个promise 或一个表明它成功完成的值。
  • 它可以throw 指示发生异常状态的错误。

它看起来像:

var doSomething = Promise.method(function(){
  if(somethingBad) throw new Error("Error operating");
  return someEventualValue(); // a direct value works here too
}); // See note, in Q you'd return Q.reject()

Promise.try(function(){ // in Q that's Q().then
  doSomething();
  console.log("Success");
}).catch(function(e){
  console.log("Boo");
});

承诺模型本身的控制流程

Promise 是对排序操作 概念本身的抽象。它描述了控制如何从一个语句传递到另一个语句。您可以将.then 视为对分号的抽象。

说说同步代码

让我们看看同步代码在您的情况下的样子。

function moduleA_exportedFunction() {
  var serviceResults = someSynchronousFunction();
    if (serviceResults.areGood) {
      // We can continue with the rest of our code
    }
    else {
      performVerySpecificErrorHandling();
      // We want to skip the rest of the chain
    }
}

那么,如何继续我们的其余代码只是returning。这在同步代码和带有 Promise 的异步代码中是相同的。执行非常具体的错误处理也可以。

我们如何跳过同步版本中的其余代码?

doA();
doB();
doC(); // make doD never execute and not throw an exception
doD();

好吧,即使不是立即,一种相当简单的方法可以通过使 doC 进入无限循环来使 doD 永远不会执行:

function doC() {
    if (!results.areGood) {
      while(true){} // an infinite loop is the synchronous analogy of not continuing
                    // a promise chain.
    }
}

因此,可能永远不会解决一个承诺——就像其他答案所暗示的那样——返回一个待处理的承诺。然而,这是非常糟糕的流量控制,因为意图很难传达给消费者,并且可能很难调试。想象一下以下 API:

moduleA_exportedFunction - 如果数据可用,此函数发出 API 请求并将服务作为 ServiceData 对象返回。否则,它使程序进入无限循环

有点混乱,不是吗:)?但是,它实际上存在于某些地方。在非常旧的 API 中发现以下内容并不少见。

some_bad_c_api() - 这个函数 foos a bar,失败时它终止进程

那么,终止 API 中的进程有什么困扰我们的呢?

一切都与责任有关。

  • 被调用的 API 负责传达 API 请求是否成功。
  • 调用者有责任决定在每种情况下做什么。

在你的情况下。 ModelA 只是违反了它的责任范围,它不应该无权对程序的流程做出这样的决定。消费它的人应该做出这些决定。

投掷

更好的解决方案是抛出错误并让消费者处理它。我将在这里使用Bluebird promises,因为它们不仅速度快了两个数量级,并且拥有更现代的 API - 它们还拥有更好的调试工具 - 在这种情况下 - 用于条件的糖捕获和更好的堆栈跟踪:

moduleA_exportedFunction().then(function(result){
   // this will only be reached if no error occured
   return someOtherApiCall();
}).then(function(result2){
   // this will be called if the above function returned a value that is not a 
   // rejected promise, you can keep processing here
}).catch(ApiError,function(e){
   // an error that is instanceof ApiError will reach here, you can handler all API
   // errors from the above `then`s in here. Subclass errors
}).catch(NetworkError,function(e){
   // here, let's handle network errors and not `ApiError`s, since we want to handle
   // those differently
}).then(function(){
   // here we recovered, code that went into an ApiError or NetworkError (assuming
   // those catch handlers did not throw) will reach this point.
   // Other errors will _still_ not run, we recovered successfully
}).then(function(){
   throw new Error(); // unless we explicitly add a `.catch` with no type or with 
                      // an `Error` type, no code in this chain will run anyway.
});

所以在一行中 - 你会做你会在同步代码中做的事情,就像 promises 通常的情况一样。

注意 Promise.method 只是 Bluebird 用于包装函数的一个便利函数,我只是讨厌同步抛出 promise 返回的 API,因为它会造成重大破坏。

【讨论】:

  • 很好的额外阐述!我认为同步示例可以从包含 try/catch 中受益。我不知道 Bluebird 有按类型捕获错误的功能,太棒了!
  • 感谢您的详细说明!我特别喜欢无限循环的类比——我知道关于未解决的承诺的想法有些不对劲,但直到现在我才发现它。但是,至于标记答案,我正在考虑自己发布一个新答案,该答案专门使用模块 A 返回的布尔值。但是,我打算对您的评论和响应给予应有的信任,以激发解决方案:)
  • David - 好主意,我已经对此进行了扩展。 @mjomble 请随意添加您自己的答案,并接受反映您如何最好地解决问题的答案。我宁愿看到同一个问题的三个解决方案,而不是三个问题的完全相同的解决方案 :) 返回布尔值是一种完全有效的策略,并且在 API 不成功不是特殊用例的情况下很有用。
  • TL;DR: throw new Error(message) 来自您的第一个错误处理函数。
【解决方案2】:

这是一种设计。通常,当一个模块或服务返回一个承诺时,如果调用成功,您希望它解决,否则失败。即使您知道调用不成功,promise 既没有解决也没有失败,这基本上是无声的失败。

但是,嘿,我不知道你的模块的细节或原因,所以如果你确实想在这种情况下静默失败,你可以通过返回一个未解决的承诺来做到这一点:

// 模块A

function moduleA_exportedFunction() {
  return promiseReturningService().then(function(serviceResults) {
    if (serviceResults.areGood) {
      // We can continue with the rest of the promise chain
    }
    else {
      performVerySpecificErrorHandling();
      // We want to skip the rest of the promise chain
      return q.defer().promise;
    }
  });
}

【讨论】:

  • +1 用于解决方案并将其称为静默失败 - 确实如此。我在抽象中添加了一个更详细的答案,但我也绝对喜欢这个。
【解决方案3】:

受 Benjamin Gruenbaum 的 cmets 和回答的启发 - 如果我用同步代码编写此代码,我会让 moduleA_exportedFunction 返回一个 shouldContinue 布尔值。

所以有了承诺,它基本上是这样的(免责声明:这是伪代码,未经测试)

// Module A

function moduleA_exportedFunction() {
  return promiseReturningService().then(function(serviceResults) {
    if (serviceResults.areGood) {
      // We can continue with the rest of the promise chain
      return true;
    }
    else {
      performVerySpecificErrorHandling();
      // We want to skip the rest of the promise chain
      return false;
    }
  });
}

// Module B

moduleA_exportedFunction()
  .then(function(shouldContinue) {
    if (shouldContinue) {
      return moduleB_promiseReturningFunction().then(moduleB_anotherFunction);
    }
  })
  .fail(function(reason) {
    // Handle the reason in a general way which is ok for module B functions
    // (And anything unhandled from module A would still get caught here)
  })
  .done()
;

它确实需要模块 B 中的一些处理代码,但逻辑既不特定于模块 A 的内部,也不涉及抛出和忽略虚假错误 - 任务完成! :)

【讨论】:

  • 记录在案 - 较新的 Promise 库不要求您明确调用 .done,而是会为您跟踪未处理的拒绝。 Q 在某个时候对此有实验性支持,但他们仍在努力 - 我认为你绝对应该考虑一个可以帮助你的库:)
猜你喜欢
  • 1970-01-01
  • 2015-07-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-27
  • 2015-07-29
  • 2018-06-01
相关资源
最近更新 更多