Promise.any 等的是首次成功:先失败的任务,为什么没有结束整组等待

前天 3阅读

先结束与先成功,是两种等待条件

同时读取几个等价数据源时,最快返回的请求可能恰好失败。Promise.race 采用第一个确定结果的任务,无论它成功还是失败;Promise.any 则等待第一个成功兑现的任务,前面的拒绝不会单独让整组失败。选哪个方法,取决于产品到底需要“最先有结果”还是“最先有可用结果”。

这个差别容易被短小演示掩盖。如果所有输入都成功,两者常常得到同一个值,看不出规则不同。有区分度的测试应该让一个任务先拒绝,再让另一个成功。下面用手动控制的 Promise 固定发生顺序,避免用网络延迟或定时器时长碰运气。

Promise.any 等的是首次成功:先失败的任务,为什么没有结束整组等待

配图为 AI 生成的概念插画:较早停下的球与首次成功抵达的球,表现两种等待条件。

让拒绝、成功和剩余任务依次发生

保存为 JavaScript 文件后用 Node.js 运行。代码在改变任务状态前,就为聚合结果注册了拒绝处理,所以预期的失败会被记录为普通输出。最后用 allSettled 等待剩余任务,专门验证胜出后没有发生自动取消。

function deferred() {
  let resolve, reject;
  const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
  return { promise, resolve, reject };
}

async function main() {
  const first = deferred(), second = deferred(), third = deferred();
  const tasks = [first.promise, second.promise, third.promise];
  const completed = [];
  const observed = tasks.map(p => p.then(
    value => { completed.push(value); return value; },
    error => { completed.push(error.message); throw error; }
  ));
  const race = Promise.race(observed).catch(e => e.message);
  const any = Promise.any(observed);
  first.reject(new Error("fast failure"));
  await Promise.resolve();
  second.resolve("usable value");
  console.log("race:", await race);
  console.log("any:", await any);
  third.resolve("still running");
  await Promise.allSettled(observed);
  console.log("completed:", JSON.stringify(completed));

  const a = deferred(), b = deferred();
  const failed = Promise.any([a.promise, b.promise]).catch(e => e);
  b.reject(new Error("B failed"));
  a.reject(new Error("A failed"));
  const error = await failed;
  console.log(error.name, JSON.stringify(error.errors.map(e => e.message)));
  try { await Promise.any([]); }
  catch (e) { console.log("empty:", e.name, e.errors.length); }
}
main().catch(e => { console.error(e); process.exitCode = 1; });

第一组输出中,race 得到 fast failure,而 any 得到 usable value。随后 still running 依旧进入完成记录。这说明组合器改变的是返回给调用者的结果条件,没有撤销原来的任务。手动控制状态只用于演示;真实请求应保留自身的错误分类、清理逻辑与资源释放路径。

全部失败时,错误数组按输入排列

第二组先拒绝输入位置靠后的 b,再拒绝 a。AggregateError 的 errors 数组仍依次显示 A failed 和 B failed,对应传入数组中 a、b 的位置,而非失败发生的时间顺序。排查多个候选数据源时,可以用这个位置关系对齐标签;若需要时间线,应另外记录时间,不要把 errors 当作事件日志。

空数组同样会产生 AggregateError,errors 长度为零,因为根本没有成功候选。它返回的是一个已拒绝的 Promise,await 或拒绝回调才会观察这个结果,并不是调用表达式同步抛出该聚合错误。可以在构建候选列表后检查长度,向上层返回更符合业务语义的“没有可用数据源”。

成功值也需要先满足你的业务条件

组合器只认识 Promise 是否兑现,不会检查对象有没有所需字段。某个请求成功返回了“未找到”的业务响应,仍可能先被选中。实际使用时,应在每个候选任务内部完成响应检查,把不合格结果转换成拒绝;这样首次兑现才真正代表应用可接受的结果。

若一个候选一直保持 pending,其余全部失败,any 仍会等待,不能把“已有多个失败”当成“已经全部失败”。超时应落实到每个候选或整体等待策略。想节省胜出后的请求开销,也必须使用相关接口支持的取消机制;取消是否安全,要结合请求是否有副作用判断,不能单靠 Promise.any 作保证。

候选任务列表还需要在调用前建立好。传入普通值也会被包装成已经兑现的候选,如果无意间混入一个缓存占位值,它可能在异步请求完成之前胜出。构建列表时应检查每项的含义,并把诊断标签与对应位置一起保存,避免错误数组到手后无法判断来自哪个来源。

成功后的返回值不是一份结果列表,而是胜出的单个值。若调用方还需要所有候选的完整诊断,应另外收集每项状态,不能再从胜出的值中还原其他任务的过程。

参考资料

ECMAScript:Promise.any 与 PerformPromiseAny;ECMAScript:Promise.race

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。