将catch放置在THEN之前和之后的位置问题

177

我不太理解在嵌套的Promise中,将.catch放置在then之前和之后的区别。

方案1:

test1Async(10).then((res) => {
  return test2Async(22)
    .then((res) => {
      return test3Async(100);
    }).catch((err) => {
      throw "ERROR AFTER THEN";
    });
}).then((res) => {
  console.log(res);
}).catch((err) => {
  console.log(err);
});

备选方案2:

test1Async(10).then((res) => {
   return test2Async(22)
     .catch((err) => {
        throw "ERROR BEFORE THEN";
      })
      .then((res) => {
        return test3Async(100);
      });
  }).then((res) => {
    console.log(res);
  }).catch((err) => {
    console.log(err);
  });
每个函数的行为如下,如果数字是<0则test1失败,如果数字是> 10则test2失败,如果数字不是100则test3失败。 在这种情况下,只有test2失败。
我尝试运行并使test2Async失败,无论是之前还是之后都会以相同的方式执行,并且不执行test3Async。 有人能解释一下在不同位置放置catch的主要区别吗?
在每个函数中,我使用console.log('Running test X')来检查它是否被执行。
这个问题是因为我之前发布的线程引起的如何将嵌套回调转换为Promise?。 我认为这是一个不同的问题,值得发表另一个主题。

无论是.then还是.catch都可以改变Promise的状态...所以我不确定误解来自哪里。如果你在.then之前放置catch,它将捕获在.then之前发生的拒绝,并且.then将根据.catch中发生的情况运行其完成/失败回调,反之亦然。 - Kevin B
抱歉如果我的问题不太清楚。但在这种情况下,正如我所说,两种情况的行为都是一样的,所以我看不出区别。您能告诉我什么时候我们将catch放在前面,什么时候我们决定将其放在后面吗?把它放在后面似乎非常直观和常见。只是不确定为什么有时我们会先把它放在前面。 - Zanko
如果它们的表现相同,那只是因为它们在这种特定情况下没有改变结果。对其中任何一个进行微小的更改都可能会改变结果。 - Kevin B
你说的“改变结果”是什么意思?抱歉,我真的很困惑哈哈。 - Zanko
例如,如果您不是抛出错误而是什么都不做,那么 Promise 将从被拒绝变为已解决。这当然会改变结果,因为 Promise 现在是一个已解决的 Promise 而不是一个被拒绝的 Promise。(除非它已经被解决,否则 catch 也不会运行) - Kevin B
在你的问题中,Promise 被拒绝了,因此 .then 的 done 回调函数不会被调用,直接跳转到 .catch。当两者交换时,.catch 运行,然后抛出一个错误,从而保持它被拒绝状态,因为和第一种情况一样,.then 的 done 回调函数再次不会运行。 - Kevin B
3个回答

371

所以,基本上你在问这两者之间有什么区别(其中p是从先前的某些代码创建的Promise):

return p.then(...).catch(...);

return p.catch(...).then(...);

p 被解析或拒绝时都存在差异,但是这些差异是否重要取决于 .then().catch() 处理程序内的代码所执行的操作。

p 被解析时会发生什么:

在第一种方案中,当 p 被解析时,会调用 .then() 处理程序。如果该处理程序返回一个值或另一个最终被解析的承诺,则跳过 .catch() 处理程序。但是,如果 .then() 处理程序抛出异常或返回最终被拒绝的承诺,则 .catch() 处理程序将针对原始承诺 p 中的拒绝以及在 .then() 处理程序中发生的错误执行。

在第二种方案中,当 p 被解析时,会调用 .then() 处理程序。如果该处理程序抛出异常或返回最终被拒绝的承诺,则 .catch() 处理程序无法捕获它,因为它在链中位于其前面。

因此,这就是区别#1。如果 .catch() 处理程序在其之后,那么它也可以捕获 .then() 处理程序中的错误。

p 被拒绝时会发生什么:

现在,在第一种方案中,如果承诺 p 被拒绝,则跳过 .then() 处理程序,并像您所期望的那样调用 .catch() 处理程序。您在 .catch() 处理程序中所做的决定将确定返回的最终结果。如果您只从 .catch() 处理程序返回一个值或返回一个最终被解析的承诺,则承诺链将切换到已解析状态,因为您“处理”了错误并正常返回了结果。如果您在 .catch() 处理程序中抛出异常或返回被拒绝的承诺,则返回的承诺保持被拒绝状态。

在第二种方案中,如果承诺 p 被拒绝,则调用 .catch() 处理程序。如果您从 .catch() 处理程序返回正常值或最终被解析的承诺(因此“处理”错误),则承诺链将切换到已解析状态,并将调用 .catch() 之后的 .then() 处理程序。

因此,这就是区别#2。如果 .catch() 处理程序在其之前,它可以处理错误,并允许调用 .then() 处理程序。

何时使用哪个方案:

如果希望只有一个 .catch() 处理程序能够捕获原始承诺 p.then() 处理程序中的错误,且来自 p 的拒绝应跳过 .then() 处理程序,则使用第一种方案。

如果想能够捕获原始承诺 p 中的错误并且(

 p.then(fn1, fn2)
这将确保仅调用fn1fn2中的一个。 如果p解析,则将调用fn1。 如果p拒绝,则将调用fn2fn1中的结果不会更改,也不能使fn2被调用或反之亦然。 因此,如果您希望无论处理程序本身发生什么情况,都可以确保仅调用两个处理程序中的一个,则可以使用p.then(fn1, fn2)

30
这个问题特别涉及到.then().catch()的顺序,你回答了这个问题。另外,你给出了一些提示,说明何时使用哪种顺序,我认为适当提到第三种选项,即将成功和错误处理程序都传递给.then()。在这种情况下,最多只会调用一个处理程序。 - ArneHugo
10
@ArneHugo - 好建议。我已经添加了。 - jfriend00
1
所以,在 Promise 链式调用中,我们可以编写 .then .catch .catch .then 这种场景吗? - Kapil Raghuwanshi
现在正在调试这样的东西。场景2 return p.catch(...).then(...);。代码首先跳转到then,然后再从p中捕获错误并跳转到catch。ES6 Promise。thencatch的顺序似乎并不重要。如果catch抛出异常也没关系,因为我们只有在then之后才会到达那里。 - Dmitry Shvedov
1
@DmitryShvedov - 正如我所猜测的那样,这个代码.then(this.setState({isModalOpen: false}))是错误的。你没有向.then()传递一个函数引用,因此括号中的代码会在承诺解决之前立即执行。正确的写法应该是.then(() => this.setState({isModalOpen: false})) - jfriend00
显示剩余5条评论

47

jfriend00的回答非常好,但我认为增加类似的同步代码是个好主意。

return p.then(...).catch(...);

与同步相似:

try {
  iMightThrow() // like `p`
  then()
} catch (err) {
  handleCatch()
}

如果 iMightThrow() 没有抛出异常,将会调用 then()。如果它抛出异常(或者 then() 本身抛出异常),那么将会调用 handleCatch()。请注意,catch 块无法控制是否调用 then

另一方面,

return p.catch(...).then(...);

类似于同步:

try {
  iMightThrow()
} catch (err) {
  handleCatch()
}

then()

在这种情况下,如果iMightThrow()没有抛出异常,那么then()将被执行。如果它抛出异常,则由handleCatch()决定是否调用then(),因为如果handleCatch()重新抛出异常,则then()将不会被调用,因为异常将立即抛给调用者。如果handleCatch()能够优雅地处理问题,则then()将被调用。


1
这是一个很好的解释,但你可以将孤立的 then() 包装在 finally{...} 中。 - trk
3
@82Tuskers,你确定吗?如果我在finally{...}中放置'then()',假设'handleCatch()'抛出异常,难道它不会被错误地调用吗?请记住,我的目标是展示类似的同步代码,并不是建议处理异常的不同方式。 - akivajgordon
那么,如果我们想要处理所有情况但仍然链式调用.then(),最好使用.then(做某事).catch(记录错误并更新状态).then(做另一件事).catch(记录错误),在每个点尝试捕获错误但继续执行语句吗? - anna

0
一些很好的答案,但并没有明确告诉我我想要的内容,因此我要补充一下。
我想知道的是,一旦捕获块被调用并返回某个值,那么剩下的.then会如何表现。
所以.catch在链中的工作方式与.then完全相同,任何由.catch返回的值都会像正常情况下的后续.then一样被处理。也就是说,对于后续的.then来说,就好像有一个前面的.then返回了该值。
这意味着在.catch中可以返回一个错误版本,例如默认值,所有后续的.then都会像处理错误版本或默认值一样处理该值。
const basePromise = new Promise((resolve, reject) => {
    throw new Error(10);
    //resolve(100);
    //reject("Ooops slipped up");
});

const myDataCall = () => basePromise.
                            catch((reason) => {
                                //console.log(reason);
                                return 500;
                            }).
                            then(x => x * 2).
                            then(x => x + 50);



const myFunc = async () => {
    const result = await myDataCall();
    console.log(`The result was: ${result}`);
}

myFunc();

这将打印出:结果是:1050。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接