DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

JavaScript 在 forEach 中 await 请求后,结果数组仍提前返回

forEach 不会等待异步回调完成,外层 await 也无法收集请求结果。根据请求依赖选择 Promise.all 或 for...of,并检查返回值、结果顺序与失败后的副作用。

请求还没结束,外层函数就返回了空数组;给 forEach 前面再加一个 await,现象仍然存在。这通常出现在用异步回调收集多个接口结果的代码里:

async function loadAll(urls, makeRequest) {
  const results = [];
  await urls.forEach(async (url) => {
    const result = await makeRequest(url);
    results.push(result);
  });
  return results;
}

这里假定 urls 是普通、非稀疏数组,makeRequest(url) 返回能代表一次请求最终结果的 Promise。代码使用支持 async / await 的 JavaScript 环境,也就是 ES2017 及之后相应实现;故障与某个 HTTP 库没有必然关系。

等待发生在回调里面

forEach 会调用回调,但不会把回调返回的 Promise 收集起来或等待它们完成。它自己的返回值是 undefined,所以外层的 await 没有得到“所有请求完成”的信号。

回调中的 await 只暂停该回调。其他回调和外层函数仍会继续。不要依赖空数组是否偶尔稍后变长来判断成功:返回一个随后还会被修改的对象,会让调用方看到与请求时序相关的结果。

若代码写成 urls.forEach(...).then(...),还会出现对 undefined 调用 then 的错误。给 results.push(...) 加 await 也不能等待请求;push 返回数组的新长度。

请求互不依赖时,返回 Promise.all 的结果

把每次请求的 Promise 放进数组,再等待这个数组中的任务:

async function loadAll(urls, makeRequest) {
  const tasks = urls.map((url) => makeRequest(url));
  return await Promise.all(tasks);
}

调用方也必须接住返回的 Promise,例如在已有异步函数里使用:

try {
  const results = await loadAll(urls, makeRequest);
  renderResults(results);
} catch (error) {
  showLoadError(error);
}

urls、makeRequest、renderResults 和 showLoadError 是应用提供的数据及函数。makeRequest 若内部启动请求却忘了 return,map 收到的仍是 undefined,这一改法也无法替它等待网络操作。核对其返回值比增加更多 await 更有用。

全部任务成功时,结果数组对应输入顺序,不按请求完成顺序排列。某个任务失败会使 Promise.all 拒绝,但其他已经启动的任务不会因此自动取消。批量写入、发消息等有副作用的操作尤其不能把这个失败理解成“全部都没执行”,随后整批重试可能造成重复操作。

必须逐项完成时,改用 for...of

后一项依赖前一项,或希望一次只发一个请求时,使用下面的版本替换 loadAll:

async function loadAll(urls, makeRequest) {
  const results = [];
  for (const url of urls) {
    const result = await makeRequest(url);
    results.push(result);
  }
  return results;
}

它会在一次请求完成后才开始下一次,任意一次拒绝都会使函数退出并向调用方传播错误。这个行为适合“失败就停止”的流程;若业务要求保留部分成功结果,应先定义返回结构和失败策略,再增加逐项捕获。

不要因为串行版本较好理解就把所有任务改成串行,也不要无条件把大量任务放进 Promise.all。应按请求之间的依赖、服务端限制和允许的并发量选择启动方式。

检查返回时机和失败路径

在测试环境让不同请求以不同顺序完成,核对调用方只在所需请求完成后消费结果。并发版本应保持输入与结果的对应关系;串行版本应在前项结束后启动后项。还要覆盖空输入、一次拒绝和 makeRequest 漏写返回值的情况。

替换前保留原函数版本。若并发造成接口限流,可以切换为串行版本,但已经发出的操作不会因代码回退而撤销。涉及写入时,应先核对已成功项目,再按业务的幂等或补偿规则处理。