如何从Promise's catch/then block返回

How to return from a Promise's catch/then block?

本文关键字:catch then block 返回 Promise      更新时间:2023-09-26

在JavaScript Promise编程时,有很多关于如何使用"then"answers"catch"的教程。然而,所有这些教程似乎都忽略了重要的一点:从then/catch块返回以打破Promise链。让我们从一些同步代码开始来说明这个问题:

try {
  someFunction();
} catch (err) {
  if (!(err instanceof MyCustomError))
    return -1;
}
someOtherFunction();

本质上,我正在测试捕获的错误,如果它不是我期望的错误,我将返回给调用者,否则程序继续。但是,这个逻辑不适用于Promise:

Promise.resolve(someFunction).then(function() {
  console.log('someFunction should throw error');
  return -2;
}).catch(function(err) {
   if (err instanceof MyCustomError) {
     return -1;
   }
}).then(someOtherFunction);

此逻辑用于我的一些单元测试,其中我希望函数以某种方式失败。即使我将catch更改为then块,我仍然无法打破一系列链接的承诺,因为从then/catch块返回的任何内容都将成为沿链传播的承诺。

我想知道Promise是否能够实现这个逻辑;如果不是,为什么?对我来说很奇怪,承诺链永远不会被打破。谢谢!

2015年8月16日编辑:根据目前给出的答案,由then块返回的被拒绝的Promise将通过Promise链传播,并跳过所有后续的then块,直到它被捕获(处理)。这种行为很容易理解,因为它简单地模仿了以下同步代码(方法1):

try {
  Function1();
  Function2();
  Function3();
  Function4();
} catch (err) {
  // Assuming this err is thrown in Function1; Function2, Function3 and Function4 will not be executed
  console.log(err);
}

然而,我要问的是同步代码中的以下场景(方法2):

try {
  Function1();
} catch(err) {
  console.log(err); // Function1's error
  return -1; // return immediately
}
try {
  Function2();
} catch(err) {
  console.log(err);
}
try {
  Function3();
} catch(err) {
  console.log(err);
}
try {
  Function4();
} catch(err) {
  console.log(err);
} 

我想以不同的方式处理不同函数中产生的错误。正如方法1中所示,我可能在一个catch块中捕获所有错误。但是那样的话,我必须在catch块中写一个大的switch语句来区分不同的错误;此外,如果不同函数抛出的错误没有一个共同的可切换属性,我将根本无法使用switch语句;在这种情况下,我必须为每个函数调用使用单独的try/catch块。方法2有时是唯一的选择。Promise的then/catch语句不支持这种方法吗?

这是无法用语言的特性实现的。然而,基于模式的解决方案是可用的。

这里有两个解决方案。

重新抛出先前的错误

这个模式基本上是健全的…

Promise.resolve()
.then(Function1).catch(errorHandler1)
.then(Function2).catch(errorHandler2)
.then(Function3).catch(errorHandler3)
.then(Function4).catch(errorHandler4)
.catch(finalErrorHandler);

Promise.resolve()不是严格必需的,但允许所有的.then().catch()行都是相同的模式,整个表达式更容易看。

…但是:

  • 如果errorHandler返回一个结果,那么链将继续到下一行的成功处理程序。
  • 如果抛出errorHandler,那么该链将继续到下一行的错误处理程序。
除非错误处理程序能够区分先前抛出的错误和新抛出的错误,否则不会发生期望的跳出链。例如:
function errorHandler1(error) {
    if (error instanceof MyCustomError) { // <<<<<<< test for previously thrown error 
        throw error;
    } else {
        // do errorHandler1 stuff then
        // return a result or 
        // throw new MyCustomError() or 
        // throw new Error(), new RangeError() etc. or some other type of custom error.
    }
}

:

  • 如果errorHandler返回一个结果,那么链将继续到下一个FunctionN。
  • 如果errorHandler抛出MyCustomError,那么它将被重复地向下抛出并被第一个不符合if(error instanceof MyCustomError)协议的错误处理程序捕获(例如最终的.catch())。
  • 如果errorHandler抛出任何其他类型的错误,那么该链将继续进行下一个捕获。

如果您需要根据抛出的错误类型灵活地跳转到链尾或不跳转到链尾,则此模式将非常有用。我想这种情况很少见。

绝缘了

另一个解决方案是引入一种机制来保持每个.catch(errorHandlerN)"绝缘",这样它将只捕获由对应的FunctionN引起的错误,而不是从任何先前的错误。

这可以通过在主链中只使用成功处理程序来实现,每个成功处理程序由一个包含子链的匿名函数组成。

Promise.resolve()
.then(function() { return Function1().catch(errorHandler1); })
.then(function() { return Function2().catch(errorHandler2); })
.then(function() { return Function3().catch(errorHandler3); })
.then(function() { return Function4().catch(errorHandler4); })
.catch(finalErrorHandler);

这里Promise.resolve()起着重要的作用。如果没有它,Function1().catch(errorHandler1)将在主链中,catch()将无法与主链隔离。

现在

  • 如果errorHandler返回一个结果,那么链将继续到下一行。
  • 如果一个errorHandler抛出了它喜欢的任何东西,那么这个链将直接进展到finalErrorHandler。

如果您希望总是跳到链的末尾,而不管抛出的错误类型是什么,请使用此模式。不需要自定义错误构造函数,也不需要以特殊的方式编写错误处理程序。

选择哪种模式取决于已经给出的考虑,也可能取决于您的项目团队的性质。

  • 一个人的团队-你写的一切,了解问题-如果你可以自由选择,然后运行你的个人偏好。
  • 多人团队——一个人写主链,其他人写函数和它们的错误处理程序——如果可以的话,选择绝缘捕获——在主链的控制下,你不需要强制以某种方式编写错误处理程序。

首先,我在这段代码中发现了一个常见的错误,它可能会让您完全困惑。这是您的示例代码块:

Promise.resolve(someFunction()).then(function() {
  console.log('someFunction should throw error');
  return -2;
}).catch(function(err) {
   if (err instanceof MyCustomError) {
     return -1;
   }
}).then(someOtherFunction());   // <== Issue here

您需要将函数引用传递给.then()处理程序,而不是实际调用函数并传递其返回结果。所以,上面的代码应该是这样的:

Promise.resolve(someFunction()).then(function() {
  console.log('someFunction should throw error');
  return -2;
}).catch(function(err) {
   if (err instanceof MyCustomError) {
     // returning a normal value here will take care of the rejection
     // and continue subsequent processing
     return -1;
   }
}).then(someOtherFunction);    // just pass function reference here

请注意,我在.then()处理程序中的函数之后删除了(),因此您只是传递函数引用,而不是立即调用函数。这将允许承诺基础结构决定将来是否调用承诺。如果你犯了这个错误,它会让你完全忘记承诺是如何工作的,因为事情会被调用。


抓住拒绝的三个简单规则。

  1. 如果没有人捕捉到拒绝,它立即停止承诺链,原始拒绝成为承诺的最终状态。没有调用后续处理程序。
  2. 如果捕获到承诺拒绝,并且不返回任何内容或从拒绝处理程序返回任何正常值,则认为拒绝已处理,承诺链继续并调用后续处理程序。无论你从拒绝处理程序返回什么,都会成为承诺的当前值,就好像拒绝从未发生过一样(除非没有调用这一级的解析处理程序——而是调用了拒绝处理程序)。
  3. 如果承诺拒绝被捕获,你抛出一个错误的拒绝处理程序,或者你返回一个被拒绝的承诺,那么所有的解析处理程序都被跳过,直到链中的下一个拒绝处理程序。如果没有拒绝处理程序,承诺链将停止,新生成的错误将成为承诺的最终状态。

您可以在这个jsFiddle中看到几个示例,其中显示了三种情况:

  1. 从拒绝处理程序返回一个常规值,导致下一个.then()解析处理程序被调用(例如正常处理继续),

  2. 抛出一个拒绝处理程序会导致正常的解析处理停止,所有的解析处理程序都被跳过,直到你到达一个拒绝处理程序或链的末端。如果在解析处理程序中发现意外错误(我认为这是您的问题),这是停止链的有效方法。

  3. 没有拒绝处理程序会导致正常的解析处理停止,所有的解析处理程序都被跳过,直到你到达拒绝处理程序或链的末端。

没有内置功能可以在请求时跳过剩余链的整个部分。但是,您可以通过在每次捕获中抛出特定的错误来模仿这种行为:

doSomething()
  .then(func1).catch(handleError)
  .then(func2).catch(handleError)
  .then(func3).catch(handleError);
function handleError(reason) {
  if (reason instanceof criticalError) {
    throw reason;
  }
  console.info(reason);
}

如果任何catch块捕获到criticalError,它们将直接跳到末尾并抛出错误。在继续下一个.then块之前,任何其他错误都将被控制台记录。

如果您可以使用较新的async await,这是相当简单的实现:

async function myfunc() {
  try {
     return await anotherAsyncFunction();
  } catch {
    //do error handling
 
    // can be async or not.
    return errorObjct();
    
  }
}
let alwaysGetAValue = await myfunc();

根据您使用的技术,您可能需要某种高级包装器函数来允许顶级await