Promise.allSettled 实战:部分任务失败后,怎样保留结果与对应关系

10-01 5阅读

商品页同时读取用户资料、库存和推荐列表时,某个辅助模块失败,页面仍可能展示其他内容。此时需要一份完整清单:哪些成功,哪些失败,每项属于谁。只在最外层捕获一次异常,通常无法直接得到这份清单。先明确页面允许哪些部分降级,再选择收集结果的方式。

Promise.allSettled 实战:部分任务失败后,怎样保留结果与对应关系

AI生成概念示意图,非真实界面

先保住任务与结果的对应关系

ECMAScript 规范规定,Promise.allSettled 会等待输入中的任务全部结束,结果对象记录成功值或失败原因,并按输入位置写回数组。下面把三个任务的完成顺序人为改成推荐、库存、资料,便于稳定复现;整个实验无需接口服务,也不依赖计时器恰好按预期触发。

任务数组同时保存 id 和 promise。收集后先按原始下标补回 id,再筛选失败项。如果先过滤成功结果,随后仍拿新下标匹配原数组,第二条记录就可能错挂到另一项任务上。用户看到的会是错误模块的状态,后续重试也可能操作错对象。

运行一个可重复的部分失败实验

将完整代码保存为 allsettled-demo.cjs,用已安装的 Node.js 执行 node allsettled-demo.cjs。三个控制函数只用于模拟接口完成,真实项目可把 promise 换成已有请求。示例明确把库存设为失败,资料和推荐成功,末尾另外验证任务工厂同步抛错的情况。

const assert = require('node:assert/strict');

async function main() {
  const completed = [];
  function task(id) {
    let succeed, fail;
    const promise = new Promise((resolve, reject) => {
      succeed = value => { completed.push(id); resolve(value); };
      fail = reason => { completed.push(id); reject(reason); };
    });
    return {id, promise, succeed, fail};
  }
  const jobs = ['profile', 'stock', 'recommendations'].map(task);
  const pending = Promise.allSettled(jobs.map(job => job.promise));
  jobs[2].succeed(3);
  jobs[1].fail(new Error('offline'));
  jobs[0].succeed({name: 'Lin'});
  const results = await pending;
  const report = results.map((result, index) => ({
    id: jobs[index].id,
    status: result.status,
    detail: result.status === 'fulfilled'
      ? result.value
      : String(result.reason instanceof Error
          ? result.reason.message : result.reason)
  }));
  const retryIds = report.filter(row => row.status === 'rejected')
    .map(row => row.id);
  assert.deepEqual(completed, ['recommendations', 'stock', 'profile']);
  assert.deepEqual(report.map(row => row.id), ['profile', 'stock', 'recommendations']);
  assert.deepEqual(retryIds, ['stock']);
  console.log('completion:', completed.join(','));
  console.log('report:', JSON.stringify(report));
  console.log('retry:', retryIds.join(','));

  const factories = [() => { throw new Error('bad config'); }, () => 7];
  const wrapped = await Promise.allSettled(
    factories.map(run => Promise.resolve().then(run))
  );
  assert.deepEqual(wrapped.map(row => row.status), ['rejected', 'fulfilled']);
  console.log('factory:', wrapped.map(row => row.status).join(','));
}
main().catch(error => { console.error(error); process.exitCode = 1; });

逐项读懂检查结果

第一行应是 completion: recommendations,stock,profile;report 中的 id 仍依次为 profile、stock、recommendations。资料保留对象值,库存保留 offline,推荐保留数字 3。retry 行只含 stock,说明重试候选来自有身份的失败记录,而不是“第几个请求先报错”。

最后一行应是 factory: rejected,fulfilled。工厂函数若直接在 map 中执行,第一项同步抛错会让数组构造提前终止;包装到 Promise.resolve().then(run) 后,异常进入对应 Promise,第二个工厂也能参与收集。这一层包装适合统一同步返回值、同步错误和异步返回值。

读取结果时先判断 status,再访问 value 或 reason。失败原因未必是 Error 对象,示例因此先做类型判断再转成文字。用于排查的内部原因和展示给访客的提示也应分别设计:页面可以显示“库存暂时不可用”,日志再记录必要的任务标识和错误细节。

把收集完成与业务完成分开

allSettled 不会替你判定业务是否成功。这里资料、推荐可展示,库存操作则应暂缓;如果三项全失败,页面应进入整体错误状态。重试还要考虑操作是否可安全重复、哪些错误值得重试,以及是否已有新请求取代旧请求,不能把所有 rejected 项无限循环重跑。

它也不提供并发上限、自动取消或截止时间。一个始终 pending 的任务会拖住整批汇总;为真实请求设置超时和取消策略时,应确认底层工作确实停止。收集一万个已启动请求的结果,并不会把并发降到十个,任务启动节奏需要另行控制。

手动验收时可把库存改为成功,确认 retry 为空;再把资料也改为失败,确认两个 id 均被保留。交换三次完成调用,结果对应关系仍应稳定。最后保留 main 的外层错误处理:输入构造、迭代或后续报表处理本身也可能出错,不能把该方法理解为整个调用链永远不会失败。

参考资料

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