链式承诺不会在拒绝时传递
Posted
技术标签:
【中文标题】链式承诺不会在拒绝时传递【英文标题】:Chained promises not passing on rejection 【发布时间】:2013-04-28 13:52:46 【问题描述】:我无法理解为什么拒绝没有通过承诺链传递,我希望有人能够帮助我理解原因。对我来说,将功能附加到一系列承诺意味着我依赖于最初的承诺来实现的意图。这很难解释,所以让我先展示一个我的问题的代码示例。 (注意:这个例子是使用 Node 和延迟节点模块。我用 Dojo 1.8.3 测试过,结果是一样的)
var d = require("deferred");
var d1 = d();
var promise1 = d1.promise.then(
function(wins) console.log('promise1 resolved'); return wins;,
function(err) console.log('promise1 rejected'); return err;);
var promise2 = promise1.then(
function(wins) console.log('promise2 resolved'); return wins;,
function(err) console.log('promise2 rejected'); return err;);
var promise3 = promise2.then(
function(wins) console.log('promise3 resolved'); return wins;,
function(err) console.log('promise3 rejected'); return err;);
d1.reject(new Error());
运行这个操作的结果是这样的输出:
promise1 rejected
promise2 resolved
promise3 resolved
好吧,对我来说,这个结果没有意义。通过附加到这个 Promise 链上,每一个都暗示了它将依赖于 d1 的成功解决和传递到链上的结果的意图。如果promise1 中的promise 没有收到wins 值,而是在它的错误处理程序中得到了一个err 值,那么链中的下一个promise 怎么可能调用它的success 函数呢?它无法将有意义的值传递给下一个 Promise,因为它本身没有获得值。
我可以用另一种方式来描述我的想法:共有三个人,John、Ginger 和 Bob。约翰拥有一家小部件商店。 Ginger 走进他的商店,要了一袋颜色各异的小玩意儿。他没有库存,所以他向他的分销商发送请求,要求将它们运送给他。与此同时,他给了 Ginger 一张雨票,说明他欠她一袋小工具。 Bob 发现 Ginger 正在获取小部件,并请求在她完成这些小部件后让他获取蓝色小部件。她同意并给了他一张纸条,说明她会的。现在,John 的经销商在他们的供应中找不到任何小部件,并且制造商不再生产它们,所以他们通知 John,John 又通知 Ginger 她无法获得这些小部件。 Bob 自己没有得到一个蓝色的小部件,怎么能从 Ginger 那里得到呢?
我对这个问题的第三个更现实的观点是。假设我有两个要更新到数据库的值。一个依赖于另一个的 id,但是在我已经将其插入数据库并获得结果之前,我无法获取该 id。最重要的是,第一个插入依赖于来自数据库的查询。数据库调用返回我用来将两个调用链接成一个序列的承诺。
var promise = db.query(parent_id: value);
promise.then(function(query_result)
var first_value =
parent_id: query_result[0].parent_id
var promise = db.put(first_value);
promise.then(function(first_value_result)
var second_value =
reference_to_first_value_id: first_value_result.id
var promise = db.put(second_value);
promise.then(function(second_value_result)
values_successfully_entered();
, function(err) return err );
, function(err) return err );
, function(err) return err );
现在,在这种情况下,如果 db.query 失败,它将调用第一个 then 的 err 函数。但随后它会调用下一个承诺的成功函数。虽然该承诺期待第一个值的结果,但它会从其错误处理函数中获取错误消息。
所以,我的问题是,如果我必须在我的成功函数中测试错误,为什么我会有一个错误处理函数?
抱歉,这篇文章太长了。我只是不知道如何用另一种方式解释它。
更新和更正
(注意:我删除了我曾经对一些 cmets 做出的回复。因此,如果有人对我的回复发表评论,他们的 cmets 可能会因为我删除它而显得断章取义。抱歉,我试图将其保留为尽可能短。)
感谢所有回复的人。我想首先向大家道歉,因为我的问题写得太差了,尤其是我的伪代码。我有点过于激进地试图保持简短。
感谢 Bergi 的回复,我想我发现了我的逻辑错误。我想我可能忽略了另一个导致我遇到的问题的问题。这可能导致承诺链的工作方式与我想象的不同。我仍在测试我的代码的不同元素,所以我什至无法形成一个正确的问题来看看我做错了什么。不过,我确实想更新你们所有人,并感谢你们的帮助。
【问题讨论】:
您使用的是什么版本?这会在 0.10.0 和延迟 0.6.3 上为我显示rejected
3 次。
它也适用于节点 0.8.3 和延迟 0.6.3 gist.github.com/Stuk/694b2377057453aa6946
【参考方案1】:
对我来说,这个结果没有意义。通过附加到这个 Promise 链上,每个 then 都暗示着它将依赖于 d1 的成功解决和传递到链下的结果
没有。您所描述的不是链,而是将所有回调附加到d1
。然而,如果你想用then
链接某些东西,promise2
的结果取决于promise1
的分辨率以及then
回调如何处理它。
文档状态:
为回调的结果返回一个新的承诺。
.then
方法通常被视为Promises/A specification(或更严格的Promsises/A+ one)。这意味着回调 shell 返回的承诺将被同化为promise2
的分辨率,如果没有成功/错误处理程序,则相应的结果将直接传递给promise2
- 所以你可以简单地 省略处理程序以传播错误。
然而,如果错误已处理,则生成的promise2
将被视为已修复,并将使用该值实现。如果您不希望这样,您将不得不re-throw
错误,就像在 try-catch 子句中一样。或者,您可以从处理程序返回一个(待)拒绝的承诺。不知道 Dojo 的拒绝方式是什么,但是:
var d1 = d();
var promise1 = d1.promise.then(
function(wins) console.log('promise1 resolved'); return wins;,
function(err) console.log('promise1 rejected'); throw err;);
var promise2 = promise1.then(
function(wins) console.log('promise2 resolved'); return wins;,
function(err) console.log('promise2 rejected'); throw err;);
var promise3 = promise2.then(
function(wins) console.log('promise3 resolved'); return wins;,
function(err) console.log('promise3 rejected'); throw err;);
d1.reject(new Error());
Bob 自己没有得到一个蓝色小部件,怎么能从 Ginger 那里得到一个?
他应该做不到。如果没有错误处理程序,他只会感知到消息(((来自 John)来自 John)来自 Ginger)没有留下任何小部件。然而,如果 Ginger 为这种情况设置了一个错误处理程序,如果 John 或他的经销商没有蓝色小部件,她仍然可以履行她给 Bob 一个小部件的承诺,给他一个来自她自己小屋的绿色小部件。
要将您的错误回调转换为元数据,来自处理程序的return err
就像是说“如果没有剩余的小部件,只需告诉他没有剩余的小部件 - 它与所需的小部件一样好”。
在数据库情况下,如果db.query失败,会调用第一个then的err函数
...这意味着错误已在此处处理。如果你不这样做,只需省略错误回调。顺便说一句,您的成功回调不会return
他们正在创建的承诺,因此它们似乎毫无用处。正确的是:
var promise = db.query(parent_id: value);
promise.then(function(query_result)
var first_value =
parent_id: query_result[0].parent_id
var promise = db.put(first_value);
return promise.then(function(first_value_result)
var second_value =
reference_to_first_value_id: first_value_result.id
var promise = db.put(second_value);
return promise.then(function(second_value_result)
return values_successfully_entered();
);
);
);
或者,因为您不需要闭包来访问先前回调的结果值,甚至:
db.query(parent_id: value).then(function(query_result)
return db.put(
parent_id: query_result[0].parent_id
);
).then(function(first_value_result)
return db.put(
reference_to_first_value_id: first_value_result.id
);
.then(values_successfully_entered);
【讨论】:
使用 angularJS 和 $q 时,throw 关键字是替换为 $q.reject(err)。 为了清理@Toilal 的评论,throw
的首选 替换是return $q.reject(err)
。 throw
我相信,仍然可以工作;它只是慢得多。【参考方案2】:
@Jordan 首先正如评论者所说,当使用延迟库时,您的第一个示例肯定会产生您期望的结果:
promise1 rejected
promise2 rejected
promise3 rejected
其次,即使它会产生你建议的输出,它也不会影响你的第二个sn-p的执行流程,这有点不同,更像是:
promise.then(function(first_value)
console.log('promise1 resolved');
var promise = db.put(first_value);
promise.then(function (second_value)
console.log('promise2 resolved');
var promise = db.put(second_value);
promise.then(
function (wins) console.log('promise3 resolved'); ,
function (err) console.log('promise3 rejected'); return err; );
, function (err) console.log('promise2 rejected'); return err;);
, function (err) console.log('promise1 rejected'); return err);
如果第一个承诺被拒绝,只会输出:
promise1 rejected
但是(进入最有趣的部分)即使延迟库肯定返回3 x rejected
,大多数其他承诺库也会返回1 x rejected, 2 x resolved
(这导致假设您通过使用获得了这些结果一些其他的承诺库)。
另外令人困惑的是,那些其他库的行为更正确。让我解释一下。
在同步世界中,“承诺拒绝”的对应物是throw
。所以语义上,异步deferred.reject(new Error())
同步等于throw new Error()
。
在您的示例中,您不会在同步回调中抛出错误,而只是返回它们,因此您切换到成功流程,错误是成功值。为确保进一步通过拒绝,您需要重新抛出错误:
function (err) console.log('promise1 rejected'); throw err; );
那么现在的问题是,为什么延迟库会将返回的错误视为拒绝?
原因是延迟中的拒绝工作有点不同。在延迟库中,规则是:promise 在通过错误实例解决时被拒绝,因此即使您执行deferred.resolve(new Error())
,它也会充当deferred.reject(new Error())
,如果您尝试执行@987654332 @ 它会抛出一个异常,说这个承诺只能在错误实例中被拒绝。这清楚地说明了为什么从then
回调返回的错误会拒绝承诺。
deferred 逻辑背后有一些合理的理由,但它仍然无法与 throw
在 javascript 中的工作方式相提并论,因此,这种行为计划在 deferred v0.7 版本中进行更改。
简短的总结:
为避免混淆和意外结果,请遵循良好实践规则:
-
始终以错误实例拒绝您的承诺(遵循同步世界的规则,在这种情况下,抛出不是错误的值被认为是一种不好的做法)。
通过抛出错误拒绝同步回调(返回它们并不能保证拒绝)。
遵守上述规定,您将在 deferred 和其他流行的 Promise 库中获得一致和预期的结果。
【讨论】:
【参考方案3】:Use 可以将错误包装在 Promise 的每一层。我在 TraceError 中链接了错误:
class TraceError extends Error
constructor(message, ...causes)
super(message);
const stack = Object.getOwnPropertyDescriptor(this, 'stack');
Object.defineProperty(this, 'stack',
get: () =>
const stacktrace = stack.get.call(this);
let causeStacktrace = '';
for (const cause of causes)
if (cause.sourceStack) // trigger lookup
causeStacktrace += `\n$cause.sourceStack`;
else if (cause instanceof Error)
causeStacktrace += `\n$cause.stack`;
else
try
const json = JSON.stringify(cause, null, 2);
causeStacktrace += `\n$json.split('\n').join('\n ')`;
catch (e)
causeStacktrace += `\n$cause`;
// ignore
causeStacktrace = causeStacktrace.split('\n').join('\n ');
return stacktrace + causeStacktrace;
);
// access first error
Object.defineProperty(this, 'cause', value: () => causes[0], enumerable: false, writable: false);
// untested; access cause stack with error.causes()
Object.defineProperty(this, 'causes', value: () => causes, enumerable: false, writable: false);
用法
throw new TraceError('Could not set status', srcError, ...otherErrors);
输出
函数
TraceError#cause - first error
TraceError#causes - list of chained errors
【讨论】:
以上是关于链式承诺不会在拒绝时传递的主要内容,如果未能解决你的问题,请参考以下文章