别再这样写 async/await 了:我在 Code Review 中见过最多的 8 个错误

我最近半年做了大概 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('全部处理完成');
}

记住:forEachmapfilterreduce 这些数组方法都不会等 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...ofPromise.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 写法是什么? 评论区说说,看看谁遇到的更离谱。

相关推荐
hoLzwEge1 小时前
node-linker VS shamefully-hoist
前端·前端框架
袋鱼不重2 小时前
解决 Web 端图片预览与下载颜色不一致的一种工程方案
前端·后端
风止何安啊2 小时前
教你用 JS + AI 实现简单的爬虫,零门槛爬取网页信息
前端
cidy_982 小时前
codebase-memory-mcp 新手完全教程:让 AI 真正「理解」你的代码库
前端
牛奶2 小时前
HTTPS你不知道的事
前端·https·浏览器
小小小小宇2 小时前
前端 Vue 如何避免不必要的子组件渲染全解析
前端
cidy_983 小时前
codebase-memory-mcp 安装教程
前端
mt_z3 小时前
Webpack 与 Vite 完全指南
前端
灏仟亿前端技术团队3 小时前
B 端多弹窗越来越难维护?试试把弹窗交互 Promise 化
前端