我最近半年做了大概 200 次 Code Review,发现一个规律:超过一半的 bug 都和 async/await 有关。不是不会写,是写错了自己都不知道。这篇文章把我见过最多的 8 个错误整理出来,附上正确写法------你大概率中过至少 3 个。
先说一个残酷的事实
async/await 是 JavaScript 里"看起来最简单,实际坑最多"的语法。它让异步代码看起来像同步的,但这恰恰是它最危险的地方------因为你会用写同步代码的思维去写异步逻辑,然后踩到一堆你意想不到的坑。
下面这 8 个错误,按我在 Code Review 中遇到的频率从高到低排。
错误 1:循环里用 await(最高频)
这是我见过最多的错误,没有之一:
javascript
// ❌ 错误写法:串行执行,3 个请求要等 3 倍时间
async function getUsers(ids) {
const users = [];
for (const id of ids) {
const user = await fetchUser(id); // 每次都等上一个完成
users.push(user);
}
return users;
}
三个独立请求,没有依赖关系,却被你写成了排队执行。假设每个请求 200ms,三个就是 600ms。
javascript
// ✅ 正确写法:并行执行,总时间 = 最慢的那个请求
async function getUsers(ids) {
const users = await Promise.all(
ids.map(id => fetchUser(id))
);
return users;
}
同样三个请求,并行执行只需要约 200ms。
但要注意 :如果请求之间有依赖关系(比如第二个请求需要第一个的结果),那就必须串行,不能用 Promise.all。判断标准很简单:后面的请求是否需要前面的返回值?
错误 2:try-catch 套娃
一个函数里套三四层 try-catch,每层 catch 里面又有 try-catch:
javascript
// ❌ 错误写法:try-catch 套娃,可读性极差
async function handleSubmit(data) {
try {
const validated = await validate(data);
try {
const result = await submitForm(validated);
try {
await sendNotification(result.id);
} catch (e) {
console.log('通知发送失败', e);
}
} catch (e) {
showError('提交失败');
}
} catch (e) {
showError('验证失败');
}
}
这种代码写出来自己都不想看第二遍。
javascript
// ✅ 正确写法:用独立的错误处理函数,或者统一 catch
async function handleSubmit(data) {
const [validateErr, validated] = await to(validate(data));
if (validateErr) return showError('验证失败');
const [submitErr, result] = await to(submitForm(validated));
if (submitErr) return showError('提交失败');
// 通知失败不影响主流程,静默处理
await sendNotification(result.id).catch(() => {});
}
// 工具函数:把 Promise 的 resolve/reject 转成 [err, data] 元组
function to(promise) {
return promise.then(data => [null, data]).catch(err => [err, null]);
}
这个 to() 工具函数大概是我复用率最高的函数,4 行代码消灭所有 try-catch 套娃。把它放到你的 utils 里,以后写 async 代码会舒服很多。
错误 3:忘记 await
这个错误隐蔽到测试都不一定能发现:
javascript
// ❌ 错误写法:忘记 await,deleteUser 返回的是 Promise 对象
async function removeUser(id) {
const result = deleteUser(id); // 忘记 await 了!
console.log(result); // 输出 Promise {<pending>}
return result;
}
更危险的版本是在条件判断里:
javascript
// ❌ 更隐蔽的错误
async function checkPermission(userId) {
const hasPermission = fetchPermission(userId); // 忘记 await
if (hasPermission) { // Promise 对象永远是 truthy!
// 这个分支永远会执行,权限检查形同虚设
doSensitiveOperation();
}
}
Promise 对象是 truthy 的,所以 if (hasPermission) 永远是 true。你以为做了权限检查,实际上完全没有。
javascript
// ✅ 正确写法
async function checkPermission(userId) {
const hasPermission = await fetchPermission(userId);
if (hasPermission) {
doSensitiveOperation();
}
}
防范方法 :开启 ESLint 的 require-await 和 @typescript-eslint/no-floating-promises 规则,编辑器会直接标红。
错误 4:在 forEach 里用 async/await
这个错误和错误 1 看起来像,但本质完全不同------forEach 里的 await 根本不会等:
javascript
// ❌ 错误写法:forEach 不会等 await,所有请求同时发出,但你拿不到结果
async function processItems(items) {
items.forEach(async (item) => {
await processItem(item); // forEach 不关心返回的 Promise
});
console.log('全部处理完成'); // 这行会立刻执行,根本没等!
}
forEach 不会等待回调函数里的 Promise 完成。上面的 console.log 会在所有 processItem 开始之前就打印出来。
javascript
// ✅ 方案一:用 for...of(串行执行)
async function processItems(items) {
for (const item of items) {
await processItem(item);
}
console.log('全部处理完成'); // 真的全部完成了
}
// ✅ 方案二:用 Promise.all + map(并行执行)
async function processItems(items) {
await Promise.all(items.map(item => processItem(item)));
console.log('全部处理完成');
}
记住:forEach、map、filter、reduce 这些数组方法都不会等 await。 需要等待的场景,要么用 for...of,要么用 Promise.all。
错误 5:catch 了但不处理
javascript
// ❌ 错误写法:吞掉错误,出了问题完全无法排查
async function fetchData() {
try {
const res = await fetch('/api/data');
return await res.json();
} catch (e) {
// 吞掉了错误,什么都不做
}
}
比"不 catch"更糟糕的是"catch 了但什么都不做"。不 catch 至少还会在控制台看到报错信息,吞掉之后出了问题你连线索都没有。
javascript
// ✅ 正确写法:至少记录错误,然后决定怎么降级
async function fetchData() {
try {
const res = await fetch('/api/data');
return await res.json();
} catch (e) {
console.error('获取数据失败:', e);
return null; // 明确返回兜底值
}
}
如果你不知道怎么处理某个错误,最好的做法是不要 catch,让它继续冒泡。 只在你知道该怎么降级的地方 catch。
错误 6:async 函数里 return 多余的 await
javascript
// ❌ 不必要的 await
async function getUser(id) {
return await fetchUser(id);
}
// ✅ 直接 return Promise 就行
async function getUser(id) {
return fetchUser(id);
}
如果一个 async 函数的最后一步就是 return 一个 Promise,那 await 是多余的。async 函数本身就会把返回值包装成 Promise。
但有一个例外:如果你的 return 在 try-catch 里面,那必须加 await,否则 catch 捕获不到错误:
javascript
// ⚠️ 这种情况必须加 await
async function getUser(id) {
try {
return await fetchUser(id); // 必须 await,否则 catch 捕获不到
} catch (e) {
return defaultUser;
}
}
去掉 await 的话,Promise 的 rejection 会在 try-catch 外面发生,catch 根本拦不住。
错误 7:不控制并发数量
错误 1 教你用 Promise.all 并行执行,但如果不控制数量,就是另一个坑:
javascript
// ❌ 危险写法:1000 个请求同时发出,接口直接炸了
async function downloadAll(urls) {
await Promise.all(urls.map(url => fetch(url)));
}
1000 个请求同时打过去,后端限流直接给你 429,浏览器也会限制同域并发数(Chrome 是 6 个)。
javascript
// ✅ 正确写法:控制并发,每次最多 5 个
async function downloadAll(urls) {
const limit = 5;
const results = [];
for (let i = 0; i < urls.length; i += limit) {
const batch = urls.slice(i, i + limit);
const batchResults = await Promise.all(
batch.map(url => fetch(url))
);
results.push(...batchResults);
}
return results;
}
分批执行是最简单的并发控制。如果你需要更精细的控制(比如某个失败了不影响其他的),可以用 Promise.allSettled 替代 Promise.all。
错误 8:混用 .then() 和 await
javascript
// ❌ 错误写法:风格混乱,可读性差
async function loadData() {
const config = await getConfig();
return fetch(config.url)
.then(res => res.json())
.then(data => {
return processData(data);
})
.catch(err => {
console.error(err);
});
}
既然用了 async/await,就统一风格,不要一半 await 一半 .then():
javascript
// ✅ 正确写法:统一用 await
async function loadData() {
const config = await getConfig();
const res = await fetch(config.url);
const data = await res.json();
return processData(data);
}
速查表:async/await 常见错误对照
| # | 错误 | 后果 | 修复 |
|---|---|---|---|
| 1 | 循环里用 await | 请求串行,速度慢 N 倍 | Promise.all + map |
| 2 | try-catch 套娃 | 可读性极差 | to() 工具函数 |
| 3 | 忘记 await | 拿到 Promise 对象,逻辑出错 | ESLint 规则自动检测 |
| 4 | forEach 里用 await | await 不生效,提前执行后续代码 | for...of 或 Promise.all |
| 5 | catch 了不处理 | 吞掉错误,无法排查 | 至少 console.error,或不 catch |
| 6 | return 多余的 await | 性能微损(例外:try 里必须加) | 最后一步直接 return |
| 7 | 不控制并发数量 | 接口被打爆,429 限流 | 分批执行或并发池 |
| 8 | 混用 .then 和 await | 风格混乱,维护困难 | 统一用 await |
一个可以直接复制的 ESLint 配置
把这几条规则加到你的 .eslintrc 里,上面大部分错误都能在编辑器里直接标红:
json
{
"rules": {
"no-await-in-loop": "warn",
"require-await": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/await-thenable": "error"
}
}
不用死记硬背,让工具帮你拦住。
最后
async/await 的语法本身不难,但它把异步代码伪装成同步代码的能力,反而让很多人放松了警惕。上面 8 个错误我自己早期也踩过大部分,后来靠 Code Review 和 ESLint 规则慢慢纠正过来的。
如果你看完发现自己中了 3 个以上,不用焦虑------说明你写的 async 代码还不够多,写多了自然就记住了。
你在 Code Review 里见过最离谱的 async/await 写法是什么? 评论区说说,看看谁遇到的更离谱。