如何从 Promise 的 catch/then 块中返回?

IT技术 javascript asynchronous promise
2021-02-28 00:25:25

有很多关于在使用 JavaScript Promise 编程时如何使用“then”和“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 块,我仍然无法破坏一系列链接的 Promise,因为从 then/catch 块返回的任何内容都将成为沿链传播的 Promise。

不知道Promise是否能够实现这个逻辑;如果不是,为什么?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 语句是否不支持这种方法?

4个回答

这不能通过语言的特性来实现。但是,可以使用基于模式的解决方案。

这里有两个解决方案。

重新抛出之前的错误

这种模式基本上是健全的......

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。

如果您想始终跳到链的末尾,而不管抛出的错误类型如何,请使用此模式。不需要自定义错误构造函数,并且不需要以特殊方式编写错误处理程序。

演示

用例

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

  • 一个人的团队——你写一切并理解问题——如果你可以自由选择,那么按照你的个人喜好运行。
  • 多人团队——一个人编写主链,其他人编写函数及其错误处理程序——如果可以,选择 Insulated Catches——一切都在主链的控制之下,你不需要强制执行以某种方式编写错误处理程序。
但我认为这是使用 Object.create 的浏览器的正确方法: myCustomError.prototype = Object.create(Error.prototype); myCustomError.prototype.constructor = myCustomError;
2021-05-04 00:25:25
很好的答案!但我认为你的第一个小提琴在 myCustomError 的实现上存在问题,它使它的行为像任何其他错误(看到它只是取消注释reject(new Error(...)。所以我认为第 5 行应该是:myCustomError.prototype = Error;
2021-05-09 00:25:25
没有滥用。像上面那样的模式很常见,对于异步流控制非常必要。
2021-05-11 00:25:25
@MarianoDesanze,非常感谢您发现这一点。我的第一个 Demo 现在使用MyCustomError()允许instanceof区分错误类型的版本。对于其他人 - 此编辑不会实质性地改变答案,但它确实使第一个演示更具说服力。
2021-05-11 00:25:25
谢谢你这么全面的回答!我已将您的回复标记为答案。但是,随着我对这个问题的进一步理解,我认为不应滥用尝试从 catch 块返回或跳过 Promise 链。Promise 链中的“then”块代表整个过程的一个步骤。如果我们想把它当作一个单独的语句来抛出唯一的错误,我们最好根本不要使用 Promise 链
2021-05-16 00:25:25

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

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()处理程序中的函数之后删除了,因此您只是传递函数引用,而不是立即调用该函数。这将允许 promise 基础结构决定是否在将来调用 promise。如果你犯了这个错误,它会完全让你对 Promise 的工作方式感到厌烦,因为无论如何都会调用它。


关于捕捉拒绝的三个简单规则。

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

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

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

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

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

@jfriend00,弹簧可以放松,而不是堆叠
2021-04-20 00:25:25
你能提供一些关于为什么 Promises 不可能的提示吗?
2021-04-22 00:25:25
@lixiang - 您甚至可以创建自己的一abort类错误约定( 的子类Error),任何中游.catch()处理程序都不应该在不重新抛出的情况下捕获该类错误但是,Promise没有内置这样的东西。
2021-04-25 00:25:25
谢谢你的捕获,我改变它!你能看看我的扩展问题吗?
2021-04-29 00:25:25
@lixiang - 您不能return用来停止Promise中的所有进一步处理。你不能因为它们的异步性质所以真的没有一个确切的类比。我已经概述了可用于 Promise 的一般选项。如果您的所有下游.catch()处理程序都检查错误的类型,并且如果它不是他们专门处理的内容,则重新抛出它,那么您可以抛出一个其他人无法处理的唯一错误,并且它将停止所有进一步的处理。它将通过所有下游.catch()处理程序,但会被每个处理程序重新抛出以最终到达链的末尾。
2021-05-09 00:25:25

没有内置功能可以在您请求时跳过整个剩余链。但是,您可以通过在每个 catch 中抛出某个错误来模仿这种行为:

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 块捕获到 a,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.