批量处理写成 await list.forEach(async ...),看起来已经“等待循环”,后面的代码却先跑了。问题不在于少写了一个 async,而在于:forEach 根本没有把每个回调的 Promise 交回来。[3]
这次在 Node.js v22.23.1 里用本地延时任务做了小实验,没有发送网络请求,也没有执行真实保存。它只验证等待和结果收集,不证明任何线上接口的速度或成功率。
外面的 await,等不到里面的任务
下面是一段可以直接运行的完整 JavaScript:
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
const done = [];
await [1, 2].forEach(async id => {
await sleep(10);
done.push(id);
});
console.log(done); // [],不是 [1, 2]
}
main().catch(console.error);
实验里,紧跟循环的读取确实得到空数组;等回调后来完成,数组才变成 [1, 2]。所以空数组并不代表这些任务被取消,只代表读取时还没等到它们。
MDN 明确说明 forEach 不等待 Promise,并且返回 undefined。回调返回了什么,不会变成 forEach 的返回结果。外面再加 await,也不会自动收集被丢弃的 Promise。[3]
需要按顺序完成:用 for...of
如果后一项依赖前一项,就让等待发生在真正的循环里。下面的 processItem 代表你已有的异步处理函数:
async function runInOrder(items, processItem) {
const results = [];
for (const item of items) {
results.push(await processItem(item));
}
return results;
}
实验中用相同类型的延时任务逐项处理,函数返回时,两项结果已经收齐。这是顺序执行的选择,不是“所有异步操作都应该串行”的规则。
任务彼此独立:用 map 收集,再 Promise.all
async function runTogether(items, processItem) {
const tasks = items.map(item => processItem(item));
return await Promise.all(tasks);
}
这里 map 把每项返回的 Promise 放进数组,Promise.all 才有一组明确的等待对象。全部成功时,结果按输入顺序排列,不按完成先后排列。[4]
本实验故意让第 2 项先完成:实际完成顺序是 [2, 1],收集到的结果仍为 [10, 20]。不要把结果数组的顺序,当成任务执行顺序的证据。
“一起等”不等于“全成功或全撤销”
Promise.all 在其中一个 Promise 拒绝时就会拒绝,并不是数据库事务。[4]另一个受控实验中,一项先失败,另一项仍在等待;捕获失败后放行后一项,它照样完成了。这只能证明本例没有自动取消,不能据此推断所有业务操作的可撤销性。
因此,批量真实写入还要单独设计失败处理。数据量大时,也不要把所有任务一次性启动:上面的简例没有并发限流,适合少量、可同时开展的独立任务,不是大批量请求方案。
我的选择标准很简单:同步副作用可以用 forEach;顺序异步用 for...of;少量独立异步用 map 加 Promise.all。先说清“什么时候算这一批完成”,再选循环写法,别靠增加 await 的数量碰运气。
资料核验:2026年10月21日。机制依据来自 MDN;结果来自 Node.js 隔离实验,不是生产事故复盘。本文由 AI 辅助整理。