Promise.all() 的一个棘手的陷阱和解决方案

本文旨在描述使用 Promise.all() 时经常出现的棘手情况,以及该问题的简单解决方案。 Promise.all() 的 MDN 页面提供了该函数的以下描述: Promise.all() 静态方法采用可迭代的 Promise 作为输入……

本文旨在描述使用时经常出现的棘手情况 Promise.all(),以及解决此问题的简单方法。

上下文

MDN 页面 Promise.all() 提供了该函数的以下描述:

Promise.all() 静态方法接受可迭代的 Promise 作为输入并返回一个 承诺。 当所有输入的 Promise 都满足时(包括传递空的可迭代对象时),返回的 Promise 就会满足,并带有一个满足值数组。 当任何输入的 Promise 拒绝时,它会拒绝,并给出第一个拒绝原因。

Promise.all() 于 2015 年发布的 ECMAScript 第六版中引入,当 Promise 首次作为语言中的内置机制添加时,但它们之前已经通过使用像 蓝鸟

ECMAScript 2020,第11版,推出 Promise.allSettled(),其行为略有不同:

Promise.allSettled() 静态方法接受可迭代的 Promise 作为输入并返回一个 承诺。 当所有输入的承诺都解决时(包括传递空的可迭代对象时),返回的承诺就会履行,并带有描述每个承诺结果的对象数组。

最后,随着 async, Promise.all() & Promise.allSettled() 可以用来 await 用于完成几个异步功能:

await Promise.all([asyncFunc1(), asyncFunc2()]) // can throw an exception

const results = await Promise.allSettled([asyncFunc1(), asyncFunc2()]) // never throw an exception

问题

Promise.all() 是一个非常方便的函数,但它在错误管理方面的行为实际上相当棘手。

然而 Promise.allSettled() 需要检查承诺的结果状态,产生的承诺 Promise.all() 将要 拒绝 如果任何输入承诺失败。 在这种情况下,这意味着 await Promise.all(...) 会抛出异常。

什么时候 Promise.all() 拒绝,一些输入的 Promise 仍然可以执行!
而这种情况往往是开发者没有考虑到的。

下面的图表和代码片段演示了这个问题:

async function sleep(durationMs) {
    return new Promise((resolve) => setTimeout(resolve, durationMs));
}

async function sleepAndFail(durationMs) {
  await sleep(durationMs);
  throw new Error(`sleepAndFail(${durationMs}) FAILED`);
};

let promiseStatus = "not-started";
async function sleepAndTrackStatus(durationMs) {
  promiseStatus = "executing";
  await sleep(durationMs);
  promiseStatus = `sleepAndTrackStatus(${durationMs}) SUCCEEDED`;
};

(async function () {
  const failing = sleepAndFail(100);
  const fastSucceeding = sleep(50);
  const slowSucceeding = sleepAndTrackStatus(200);
  try {
    console.log(await Promise.all([
      failing,
      fastSucceeding,
      slowSucceeding,
    ]));
  } catch (error) {
    console.log(error); // -> Error: sleepAndFail(100) FAILED
  }
  console.log("Final promiseStatus for slowSucceeding:", promiseStatus); // -> executing!
})()

问题是,当输入承诺失败时, Promise.all() 将要 尽早拒绝,无需等待其他承诺,仍然可以异步处理。

这可能会导致许多有问题的情况,其中其他 Promise 执行的代码执行的操作可能与调用后执行的代码发生冲突 Promise.all()。 例如,数据库连接或文件 I/O 问题是可以预料的。

就我而言,我在级联故障的情况下发现了这个根本问题 测试套件:一些异步代码 afterEach() 当某些单元测试由于调用而失败时,方法没有正确等待共享资源被清理 await Promise.all()

根本原因

出于好奇,我试图找出实现的源代码 Promise.all()。 我认为它的大部分逻辑都在文件中 src/builtins/promise-all.tq 在 V8 JavaScript 引擎中。
.tq 文件写入 扭矩,一种特定于 V8 的高级语言,可转换为 C++。 因此,我发现很难弄清楚“短路”承诺拒绝逻辑在实现中到底发生在哪里。

对于好奇的读者,优秀的文章中有一个关于 Torque 语言的部分 Learning-v8 存储库,作者:Daniel Bevenius

一个办法

对于这个问题, Promise.allSettled() 是一个更好的替代品 Promise.all(),因为它等待作为参数传递给该函数的所有承诺的完成。 实际上, allSettled 正是由于这个原因,EcmaScript 被引入,因为 其原始提案文件 揭示了:

Promise.allSettled 其独特之处在于始终等待其所有输入值。

然而, Promise.allSettled() 要求您自己处理潜在的拒绝,这很容易被忘记,或者创建重复的样板代码。

我建议的解决方案是 一个简单的安全替代品 Promise.all():

/*
 * This function ensures that (1) all the promises provided have completed
 *                        and (2) that a rejection is produced if at least one of those promises is rejected.
 */
async function waitForPromises<T>(promises: Iterable<PromiseLike<T>>) {
  const results = await Promise.allSettled(promises);
  const rejectedResults: PromiseRejectedResult[] = results.filter(
    (result): result is PromiseRejectedResult => result.status === "rejected"
  );
  if (rejectedResults.length === 1) {
    throw rejectedResults[0].reason;
  }
  if (rejectedResults.length > 1) {
    throw new AggregateError(rejectedResults.map((result) => result.reason), `${rejectedResults.length} promises failed`);
  }
  const successfullResults: PromiseFulfilledResult<Awaited<T>>[] = results.filter(
    (result): result is PromiseFulfilledResult<Awaited<T>> => result.status === "fulfilled"
  );
  return successfullResults.map((result) => result.value);
}

这其实是 带有泛型的 TypeScript,但你可以删除 : types 获取有效的 Javascript 代码:

JavaScript 中的 waitForPromises
async function waitForPromises(promises) {
  const results = await Promise.allSettled(promises);
  const rejectedResults = results.filter(result => result.status === "rejected");
  if (rejectedResults.length === 1) {
    throw rejectedResults[0].reason;
  }
  if (rejectedResults.length > 1) {
    throw new AggregateError(rejectedResults.map((result) => result.reason), `${rejectedResults.length} promises failed`);
  }
  return results.map((result) => result.value);
}

您可以通过替换来测试此功能 Promise.all 经过 waitForPromises 在本文的最初代码片段中。

虽然有时会出现“短路”行为 Promise.all 可以方便,我认为 waitForPromises 在大多数情况下是更好、更安全的选择,并且应该是首选的默认选项 await 完成几个异步功能。

(谢谢 Reddit 用户@senoptic 有关此博文的非常相关的反馈)

进一步阅读

Leave a Reply

Your email address will not be published. Required fields are marked *

近期新闻​

编辑精选​