链式Promise不会传递拒绝

IT技术 javascript node.js dojo promise deferred
2021-01-25 12:24:25

我在理解为什么拒绝不通过Promise链传递时遇到问题,我希望有人能够帮助我理解原因。对我来说,将功能附加到Promise链意味着我依赖于要实现的原始Promise的意图。这很难解释,所以让我先展示我的问题的代码示例。(注意:此示例使用 Node 和延迟节点module。我使用 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 怎么可能调用其成功函数?它无法将有意义的值传递给下一个Promise,因为它本身没有获得值。

我可以用另一种方式来描述我的想法:有三个人,John、Ginger 和 Bob。约翰拥有一家小部件商店。Ginger 走进他的商店,要了一袋各种颜色的小工具。他没有库存,所以他向他的经销商发送请求,让他们将它们运送给他。与此同时,他给了 Ginger 一张雨票,说他欠她一袋小部件。Bob 发现 Ginger 正在获取小部件并要求他在她完成后获取蓝色小部件。她同意了,并给了他一张纸条,说明她会同意的。现在,John 的经销商在他们的供应中找不到任何小部件,制造商也不再生产这些小部件,因此他们通知 John,John 又告诉 Ginger 她无法获得这些小部件。当鲍勃自己没有得到蓝色小部件时,她怎么能从金杰那里得到一个蓝色小部件?

我对这个问题的第三个更现实的观点是这个。假设我有两个要更新到数据库的值。一个依赖于另一个的 id,但是在我已经将它插入数据库并获得结果之前我无法获取 id。最重要的是,第一个插入依赖于来自数据库的查询。数据库调用返回我用来将两个调用链接成一个序列的Promise。

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 函数。但是它会调用下一个Promise的成功函数。虽然该Promise期待第一个值的结果,但它会从其错误处理函数中获取错误消息。

所以,我的问题是,如果我必须测试成功函数中的错误,为什么我会有错误处理函数?

对不起,这太长了。我只是不知道如何用另一种方式来解释它。

更新和更正

(注意:我删除了我曾经对一些评论所做的回复。所以如果有人对我的回复发表评论,那么他们的评论在我删除后可能看起来是断章取意的。抱歉,我试图保持尽可能简短.)

谢谢回复的各位。我首先向大家道歉,因为我的问题写得如此糟糕,尤其是我的伪代码。我试图保持简短,有点过于激进。

感谢Bergi的回应,我想我发现了我的逻辑错误。我想我可能忽略了导致我遇到的问题的另一个问题。这可能导致Promise链的工作方式与我认为的不同。我仍在测试我的代码的不同元素,所以我什至无法形成一个正确的问题来看看我做错了什么。不过,我确实想为大家更新,并感谢您的帮助。

3个回答

对我来说,这个结果没有意义。通过附加到这个Promise链,每一个都暗示了它将依赖于 d1 的成功解析和沿链传递的结果的意图

不。您所描述的不是链,而是将所有回调附加到d1. 然而,如果你想用 链接一些东西then,结果promise2取决于 的分辨率promise1 then回调处理它的方式

文档状态:

返回回调结果的新Promise。

.then方法通常根据Promises/A 规范(或更严格的Promises/A+规范)来看待这意味着回调 shell 返回的Promise将被同化为 的分辨率promise2,如果没有成功/错误处理程序,则相应的结果将直接传递给promise2- 因此您可以简单地省略处理程序来传播错误。

然而,如果错误被处理,结果promise2将被视为固定的,并将用该值实现。如果您不希望那样,您将不得不重新throw错误,就像在 try-catch 子句中一样。或者,您可以从处理程序返回一个 (to-be-)rejected promise。不确定拒绝 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());

当鲍勃自己没有得到蓝色小部件时,她怎么能从金杰那里得到一个蓝色小部件?

他应该做不到。如果没有错误处理程序,他只会感知到消息(((来自约翰的分发者)来自 Ginger))没有小部件剩余。然而,如果 Ginger 为这种情况设置了一个错误处理程序,如果 John 或他的经销商处没有蓝色小部件,她仍然可以通过从她自己的小屋里给他一个绿色小部件来履行她给 Bob 一个小部件的Promise。

要将您的错误回调转换为隐喻,return err来自处理程序就像说“如果没有小部件剩余,只需给他说明没有剩余小部件 - 它与所需的小部件一样好”。

在数据库情况下,如果 db.query 失败,它会调用 first then 的 err 函数

...这意味着错误在那里处理。如果你不这样做,只需省略错误回调。顺便说一句,您的成功回调不是return他们正在创建的Promise,因此它们似乎毫无用处。正确的应该是:

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);
当使用带有 $q 的 angularJS 时,throw 关键字将替换为 $q.reject(err)。
2021-03-21 12:24:25
要清理@Toilal 的评论,首选替换throw, 是return $q.reject(err)throw我相信,它仍然有效;它只是慢得多。
2021-03-24 12:24:25

@Jordan 首先正如评论者指出的那样,在使用延迟库时,您的第一个示例肯定会产生您期望的结果:

promise1 rejected
promise2 rejected
promise3 rejected

其次,即使它会产生您建议的输出,它也不会影响您的第二个代码段的执行流程,这有点不同,更像是:

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

并且,在第一个Promise被拒绝的情况下,只会输出:

promise1 rejected

然而(进入最有趣的部分)即使延迟库肯定会返回3 x rejected,大多数其他Promise库也会返回1 x rejected, 2 x resolved(这导致假设您通过使用其他Promise库来获得这些结果)。

更令人困惑的是,那些其他库的行为更正确。让我解释。

在同步世界中,“Promise拒绝”的对应物是throw. 所以在语义上,异步deferred.reject(new Error())同步等于throw new Error(). 在您的示例中,您不会在同步回调中​​抛出错误,而只是返回它们,因此您切换到成功流程,错误是成功值。为了确保拒绝被进一步传递,您需要重新抛出您的错误:

function (err) { console.log('promise1 rejected'); throw err; });

那么现在的问题是,为什么延迟库将返回的错误视为拒绝?

原因是延迟中的拒绝工作有点不同。在 deferred lib 中,规则是:promise 在使用 error 实例解决时被拒绝,所以即使你这样做,deferred.resolve(new Error())它也会作为deferred.reject(new Error()),如果你尝试这样做deferred.reject(notAnError)会抛出一个异常,说这个Promise只能被实例拒绝的错误。这清楚地说明了为什么从then回调返回的错误拒绝了Promise。

deferred 逻辑背后​​有一些有效的推理,但它仍然与throwJavaScript 中的工作方式不符,因此这种行为计划在 deferred 的 v0.7 版本中进行更改。

简短的摘要:

为避免混淆和意外结果,只需遵循良好实践规则:

  1. 总是用错误实例拒绝你的Promise(遵循同步世界的规则,抛出不是错误的值被认为是一种不好的做法)。
  2. 通过抛出错误拒绝同步回调(返回它们并不能保证拒绝)。

遵守上述规定,您将在延迟和其他流行的Promise库中获得一致和预期的结果。

使用可以包装 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