聊聊 fetch 使用中我踩过的那些坑和正确打开方式

你是不是也遇到过这种情况:后端小哥信誓旦旦说接口没毛病,你自己用Postman试了也正常,可前端就是拿不到数据。

控制台一看,报的错牛头不对马嘴,什么“Failed to fetch”…… 更气人的是,明明写了 .catch(),错误却没抓住,眼睁睁看着页面白屏。

🎯 先说核心:fetch这玩意儿到底坑在哪儿

记得我刚开始写前端那会儿,一听原生的 fetch 比 XMLHttpRequest 好用,立马把项目里的 axios 全换了。结果项目一运行,报错信息差点没把我晃晕。

最核心的一个坑,也是90%新手会掉的——fetch 只把网络错误算作 reject,HTTP错误状态(比如404、500)它照样当成功处理。

换句话说,哪怕服务器返回404,fetch的Promise仍然会走.then(),你要是不手动判断,后面一堆代码就拿着 undefined 开心地执行下去了。

这篇文章不跟你背文档,咱就像朋友聊天一样,把我这些年对 fetch 的理解、踩过的坑、还有现在一直用的“稳妥写法”,全倒给你。

📦 把fetch想象成点外卖

没错,我觉得 fetch 特别像点外卖。你下单(发请求),平台给你个订单号(Promise),外卖小哥会不会送到你家呢?不一定,可能商家自己取消订单了,但平台会告诉你“订单已生成”。

fetch 的 Response 对象就是这个“订单详情”。你得自己打开看看里面的 status (200?404?)和 ok 字段,不然外卖没送到你还给人打五星好评呢。

🛠️ 来,直接上我项目里在用的“安全代码”

咱们不废话,我先给你看一段可以直接复制的异步请求封装,然后再拆开说里头的门道。

async function request(url, options = {}) {
  try {
    const response = await fetch(url, options);
    // 重点1:先判断HTTP状态码
    if (!response.ok) {
      // 顺手把服务器返回的错误信息抛出去
      const errorBody = await response.text();
      throw new Error(`请求失败 ${response.status}: ${errorBody}`);
    }
    // 重点2:根据预期类型解析
    const contentType = response.headers.get("content-type");
    if (contentType && contentType.includes("application/json")) {
      return await response.json();
    }
    return await response.text();
  } catch (error) {
    // 重点3:在这儿统一处理上报和提示
    console.error("请求出错:", error);
    throw error; // 继续抛给调用方,别默默吞掉
  }
}

🎯 状态判断一定要用 response.ok

官方给了个现成属性 response.ok ,相当于 status 在 200-299 之间。千万别自己手写 if (status === 200) ,我早期吃过亏,碰到 201 Created 这种成功状态就直接卡死了。

📨 返回值的“一次性消费”特性

你可能会问,为啥上面我先把错误信息用 text() 读了,外面还能正常 json() ?因为我在抛错误前就已经消费掉了。

这引出 fetch 最容易翻车的点——Response 的 .json()、.text()、.blob() 都只能调一次。想既看原始文本又当 JSON 用?不行!要么先 clone() 一份,要么像我这样,只在确定是错误时才读一次。

再说个真实案例:有次我调试接口,想在控制台打印一下原始返回值,于是提前用 await response.json() 把数据拿出来了,接着返回给业务代码再用 .json() 一次,结果直接报错。调试了半天才发现是因为 body 流已经被读完了。

🔐 别忘了解析的那点小心思

经常有人问我:为啥后端明明返回了JSON,我的代码还是报错?

很多情况是后端在返回 401 或 403 时,虽然状态码不对,但 body 还是 JSON 格式的错误信息。如果你不先用 text() 看一眼直接 json(),很可能因为 content-type 不匹配而解析失败,结果连真正的错误都看不到。

我的习惯是,在非 ok 分支里一律先用 text() 拿到原始内容,既安全,又能把服务器给的提示原样显示出来,用户体验好不少。

🧵 最后啰嗦一句关于async/await

是不是以为写成 async/await 就万事大吉了?我可见过太多同学在 useEffect 里直接 async,或者 forEach 里用 await,结果数据像没睡醒一样迟迟不来。

如果你在循环里发请求,老老实实用 for...of 配合 await,或者用 Promise.all 并行处理。不然你可能会度过一个非常崩溃的调试下午。

拿我最开始翻车的代码举例吧,当时想批量拉取用户详情,写法大概长这样:

// ❌ 错误示范:forEach里用await,毛用没有
const userIds = [1, 2, 3];
const users = [];

userIds.forEach(async (id) => {
  const res = await fetch(`/api/user/${id}`);
  const data = await res.json();
  users.push(data);
});
// 这时候users大概率还是个空数组,因为forEach不等await
console.log(users); // 惊不惊喜?意不意外?

以为这样就能一个个按顺序拿到数据,但结果,天真了。forEach 的回调被标记为 async 后,并不会让外层的同步代码等它执行完,相当于你安排了三个人同时出门办事,还没等他们回来你就直接宣布“任务全部完成”了。

后来学乖了,改成了两种稳妥写法:

// ✅ 按顺序一个个来(适合有依赖的情况)
for (const id of userIds) {
  const res = await fetch(`/api/user/${id}`);
  const data = await res.json();
  users.push(data);
}

// ✅ 或者不关心顺序,全扔出去并行跑
const promises = userIds.map(id => 
  fetch(`/api/user/${id}`).then(res => res.json())
);
const users = await Promise.all(promises);

记住了,只要在循环里用 await,老老实实放弃 forEach,拥抱 for...of,或者直接用 map + Promise.all。这个坑我替你踩过了,别再往里掉啦!!!


好啦,今天掏心窝子的分享就到这儿。如果你在 fetch 上还躺过其他奇奇怪怪的坑,或者对我的封装有啥改进建议,特别欢迎给我留言。要是觉得这篇唠嗑能帮你省几根头发,动动手指点个“赞”,别让它在收藏夹吃灰呀~ 🌟

posted @ 2026-05-26 14:06  一名程序媛呀  阅读(93)  评论(0)    收藏  举报