为啥 await 不能用在 forEach 中
Category(分类): JavaScript Status: 已整理
更准确的标题应该是:
forEach不会等待async回调返回的 Promise。await当然可以写在forEach的async回调内部,只是外层的forEach和包含它的函数不会自动等待这些 Promise。
一、原文示例发生了什么
function fetchValue(value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), 500 * value)
})
}
function test() {
const values = [3, 2, 1]
values.forEach(async (value) => {
const result = await fetchValue(value)
console.log(result)
})
console.log('end')
}
test()
输出通常是:
end
1
2
3
forEach 会同步地依次调用三个回调。每个 async 回调第一次执行到 await 时就返回一个 Promise,forEach 不会保存、更不会等待这个返回值,于是外层函数立即打印 end。三个定时器的延迟分别是 1500、1000、500 毫秒,所以结果按完成时间输出 1、2、3。
原文把原因概括成“forEach 只支持同步代码”,这不够准确:回调中可以执行异步代码,问题只是 forEach 的返回值始终是 undefined,不会建立 Promise 链,也不会把回调的异常传给调用者。

二、需要并行执行:map + Promise.all
如果任务之间互不依赖,希望同时开始并在全部完成后继续,应把每个异步回调的 Promise 收集起来:
function fetchValue(value) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), 500 * value)
})
}
async function runInParallel() {
const values = [3, 2, 1]
const results = await Promise.all(
values.map((value) => fetchValue(value))
)
console.log(results) // [3, 2, 1]:结果顺序与输入一致
console.log('end')
}
runInParallel().catch(console.error)
这里三个任务几乎同时开始,因此约 500 毫秒后最先完成值为 1 的任务,但 Promise.all 的结果数组仍按输入顺序排列。若在任务内部 console.log,日志仍可能按完成顺序出现:
async function logInParallel() {
const values = [3, 2, 1]
await Promise.all(
values.map(async (value) => {
const result = await fetchValue(value)
console.log(result) // 通常为 1、2、3
return result
})
)
console.log('end') // 所有任务完成后才执行
}
所以原文说“Promise.all 不行”需要改成:如果期望串行完成,Promise.all 不符合目标;如果期望并行开始并等待全部结束,Promise.all 正是合适的方案。

错误处理
async function runAll() {
const values = [1, 2, 3]
try {
await Promise.all(
values.map(async (value) => {
if (value === 2) throw new Error('value 2 failed')
return fetchValue(value)
})
)
} catch (error) {
console.error('至少一个任务失败:', error.message)
}
}
Promise.all 在一个输入 Promise rejected 后会尽快 rejected,但其他已经启动的任务不会被自动取消。需要知道每一项结果而不是遇到第一项失败就结束时,可以使用 Promise.allSettled:
const results = await Promise.allSettled(
[1, 2, 3].map((value) => fetchValue(value))
)
for (const result of results) {
if (result.status === 'fulfilled') {
console.log('success:', result.value)
} else {
console.error('failed:', result.reason)
}
}
三、需要串行执行:for...of
如果后一个任务依赖前一个任务的结果,或者服务端要求按顺序请求,可以使用 for...of:
async function runSerially() {
const values = [3, 2, 1]
for (const value of values) {
const result = await fetchValue(value)
console.log(result) // 3、2、1
}
console.log('end')
}
runSerially().catch(console.error)
for...of 每次循环体都属于同一个 async 函数执行流程。当前一轮遇到 await 时,函数暂停;Promise fulfilled 后,函数才恢复并进入下一轮。它不是因为“使用了迭代器就自动支持 await”,真正起作用的是 await 所在的 async 函数以及循环体之间没有被丢弃的控制流。

普通 for 循环也可以实现同样的效果:
async function runWithFor() {
const values = [3, 2, 1]
for (let index = 0; index < values.length; index += 1) {
const result = await fetchValue(values[index])
console.log(result)
}
}
如果循环中需要使用索引、break、continue 或修改循环变量,普通 for 可能更方便。for...of 和 for 都支持 break 和 continue,而 forEach 的回调中不能用它们控制外层遍历。

四、限制并发数
全部并行可能压垮浏览器连接、服务端或数据库;全部串行又可能太慢。可以启动固定数量的 worker,按输入顺序领取任务:
async function mapWithConcurrency(items, limit, worker) {
if (!Number.isInteger(limit) || limit < 1) {
throw new RangeError('limit must be a positive integer')
}
const results = new Array(items.length)
let nextIndex = 0
async function consume() {
while (true) {
const index = nextIndex
nextIndex += 1
if (index >= items.length) return
results[index] = await worker(items[index], index)
}
}
const workerCount = Math.min(limit, items.length)
await Promise.all(
Array.from({ length: workerCount }, () => consume())
)
return results
}
async function demoLimitedConcurrency() {
const values = [1, 2, 3, 4, 5]
const results = await mapWithConcurrency(values, 2, async (value) => {
await fetchValue(value)
return value * 2
})
console.log(results) // [2, 4, 6, 8, 10]
}
demoLimitedConcurrency().catch(console.error)
上例使用了顶层 await,需要放在 ES 模块中运行;在 CommonJS 中可以包进一个 async 函数。并发限制只控制同时运行的 worker 数量,不会自动取消已经开始的任务。
五、什么时候用哪一种
| 目标 | 推荐写法 | 特点 |
|---|---|---|
| 不关心异步结果,也不需要等待 | forEach 中调用明确的副作用函数 | 必须自行处理错误和生命周期,不推荐丢弃 Promise |
| 所有任务并行,等全部完成 | Promise.all(values.map(worker)) | 快,但可能产生大量并发 |
| 所有任务完成,无论成功失败 | Promise.allSettled | 可逐项检查状态 |
| 按顺序执行 | for...of 或普通 for | 易读,可 break/continue |
| 控制并发量 | worker pool 或并发限制库 | 在吞吐量和资源占用之间折中 |
当任务产生了副作用(写入、删除、发送请求)时,不要为了“看起来简洁”把 Promise 丢给 forEach。明确选择并行、串行或限并发策略,并在最外层处理 rejected Promise。
六、for await...of 的补充
for await...of 用于消费异步可迭代对象,也可以消费普通可迭代对象中的 Promise。它适合流、分页器和异步生成器,不是普通数组异步遍历的唯一答案:
async function* valuesFromServer() {
yield fetchValue(1)
yield fetchValue(2)
yield fetchValue(3)
}
for await (const value of valuesFromServer()) {
console.log(value)
}
异步迭代器还可能实现 return(),因此在提前 break 时可以进行资源清理。处理文件流、网络流时,应同时了解具体 API 的取消和关闭机制。
总结
await 不是不能写在 forEach 中,而是 forEach 不会等待 async 回调返回的 Promise。选择循环方式时先明确需求:
- 需要串行:
for...of或for。 - 需要并行并等待全部:
Promise.all。 - 需要收集失败和成功:
Promise.allSettled。 - 需要控制资源:限制并发数。
参考资料:
原文作者:iDo。参考:async,await 与 forEach 引发的血案。