每天都在写的 await,你确定你用对了吗
本文揭示了Node.js项目中5个常见的异步编程陷阱:1)循环中串行await导致性能问题,应改用Promise.all并行处理;2)Promise.all的"一挂全挂"特性,推荐使用Promise.allSettled;3)无关联请求被写成瀑布式串行;4)不必要地await非关键操作;5)错误处理不当导致错误被吞没。文章提供了每个问题的具体解决方案,并给出3个自查要点:检查await依赖关系、确
有个事情我一直想聊聊。
前段时间帮一个朋友看他的 Node.js 项目,有个接口特别慢——用户列表页加载要 6 秒多。我一开始以为是数据库查询的问题,结果打开 Chrome DevTools 一看,数据库查询本身只花了 200ms。
那剩下的 5 秒多去哪了?
我翻了翻代码,发现问题出在一个看起来完全"正常"的 for...of 循环里。他在循环里一个一个地 await,本来可以并行跑完的 10 个请求,被他写成了排队等候。
老实讲,这种写法我自己以前也干过。而且我敢打赌,正在看这篇文章的你,项目里大概率也有类似的代码。
陷阱一:循环里的 await 是个隐形杀手
先看一段几乎每个人都写过的代码:
// ❌ 看起来没毛病,但慢得离谱
async function getUserProfiles(userIds) {
const profiles = [];
for (const id of userIds) {
// 每次循环都要等上一个请求完成才发下一个
const profile = await fetchProfile(id);
profiles.push(profile);
}
return profiles;
}
这段代码的问题在于:假设 fetchProfile 每次耗时 500ms,10 个用户就是 5000ms 串行等待。
但这 10 个请求之间根本没有依赖关系。它们完全可以同时发出去。
改一种写法,效果天差地别:
// ✅ 并行请求,快了不止一点
async function getUserProfiles(userIds) {
const promises = userIds.map(id => fetchProfile(id));
// 所有请求同时发出,等最慢的那个完成就行
const profiles = await Promise.all(promises);
return profiles;
}
同样 10 个请求,并行跑的话总耗时约等于单个最慢请求的时间,大概 500-600ms。从 5 秒到 0.5 秒,差了将近 10 倍。
就改了两行代码。
陷阱二:Promise.all 的"一挂全挂"问题
不过用 Promise.all 也不是没有坑。
它有一个很多人不知道(或者知道但没当回事)的特性:只要有一个 Promise 失败,整个 Promise.all 就会立即 reject,其他已经成功的结果你也拿不到。
// ⚠️ 如果第3个用户的请求失败了
// 前2个成功的结果也会被丢掉
async function getUserProfiles(userIds) {
try {
const profiles = await Promise.all(
userIds.map(id => fetchProfile(id))
);
return profiles;
} catch (error) {
// 这里只能拿到第一个失败的错误
// 成功的那些结果?没了
console.error('有一个请求挂了:', error);
return [];
}
}
如果你的业务场景是"10 个里挂了 1 个也没关系,其他 9 个该展示还得展示",那 Promise.all 就不合适了。
这时候该用的是 Promise.allSettled:
// ✅ 不管成功失败,全部结果都拿到
async function getUserProfiles(userIds) {
const results = await Promise.allSettled(
userIds.map(id => fetchProfile(id))
);
// 每个结果都有 status 字段:'fulfilled' 或 'rejected'
const profiles = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failures = results
.filter(r => r.status === 'rejected');
if (failures.length > 0) {
console.warn(`${failures.length} 个请求失败,但不影响其他结果`);
}
return profiles;
}
Promise.allSettled 会等所有 Promise 都结束——不管成功还是失败——然后把每个结果都完整地告诉你。你可以自己决定怎么处理失败的那几个。
这在做列表页、批量操作、数据聚合的时候特别有用。
陷阱三:无意识的瀑布式 await
这个坑更隐蔽。看看这段代码,你觉得有什么问题?
// ❌ 看起来像是在一步步处理数据
// 实际上是无意识的串行瀑布
async function loadDashboard() {
const user = await fetchUser();
const orders = await fetchOrders();
const notifications = await fetchNotifications();
return { user, orders, notifications };
}
三个请求之间有依赖关系吗?没有。用户信息、订单列表、通知列表——三个完全独立的数据源。
但因为你一行一行 await,它们就变成了:先等用户 → 再等订单 → 最后等通知。三个各 300ms 的请求,加起来就是 900ms。
怎么改?
// ✅ 三个独立请求并行发出
async function loadDashboard() {
const [user, orders, notifications] = await Promise.all([
fetchUser(),
fetchOrders(),
fetchNotifications(),
]);
return { user, orders, notifications };
}
改完之后总耗时 ≈ 300ms。快了 3 倍。
判断标准其实很简单:两个 await 之间,后面那个是否依赖前面那个的结果?如果不依赖,它们就应该并行。
陷阱四:await 不是万能胶水
还有一种场景经常被忽略。有时候你 await 了一个根本不需要等待的操作:
// ❌ 日志记录不需要等待——你又不用它的返回值
async function processOrder(order) {
const result = await saveOrder(order);
// 记录日志非得等它写完吗?
await logActivity('order_created', order.id);
return result;
}
logActivity 是个写日志的操作——你不需要它的返回值,也不需要等它完成才继续。这是一个典型的"发射后不管"(fire-and-forget)的场景。
// ✅ 不需要等待的操作就别等
async function processOrder(order) {
const result = await saveOrder(order);
// 用 void 明确表示"我知道这是个 Promise,我故意不等它"
void logActivity('order_created', order.id);
return result;
}
用 void 前缀可以消除 ESLint 的 no-floating-promises 警告,同时也让代码意图更清晰:我知道这是异步操作,我故意不 await 它。
当然,如果日志写入失败会影响业务,那你还是得 await。但大多数场景下,日志、埋点、统计这类操作完全没必要阻塞主流程。
陷阱五:错误处理的"吞错"问题
最后一个,也是最容易被忽视的。
// ❌ 看起来做了错误处理,但其实在吞错
async function fetchData() {
try {
const data = await riskyApiCall();
return data;
} catch (error) {
console.log(error); // 就打了个 log 然后呢?
// 没有 return,没有 throw
// 调用方拿到的是 undefined
}
}
调用 fetchData() 的人以为拿到了数据,结果拿到了 undefined,然后这个 undefined 一路传下去,在某个不相关的地方爆出 Cannot read property 'xxx' of undefined。
你 debug 半天,发现根因在这段看起来"做了错误处理"的 try-catch 里。
// ✅ 要么重新抛出,要么返回明确的错误标记
async function fetchData() {
try {
const data = await riskyApiCall();
return data;
} catch (error) {
console.error('请求失败:', error.message);
// 方案A:重新抛出,让调用方决定怎么处理
throw error;
// 方案B:返回明确的错误标记
// return { error: true, message: error.message };
}
}
要么把错误重新抛出去让上层处理,要么返回一个明确的标记告诉调用方"我失败了"。千万别 catch 了之后啥也不干——这比不写 try-catch 还危险。
一个简单的自查清单
下次写 await 的时候,花 3 秒问自己三个问题:
- 这几个 await 之间有依赖关系吗? 没有 → 用
Promise.all - 这个操作的结果我真的需要等吗? 不需要 →
void发出去就行 - 我的 catch 里有没有吞掉错误? 有 → 要么 throw 要么返回明确标记
这三个问题,可能帮你省掉一半的接口响应时间,还有好几个小时的 debug 时间。
你们项目里有没有在循环里写 await 的代码?改完快了多少?评论区聊聊。
更多推荐




所有评论(0)