文章目录

JavaScript 网络问题:fetch 请求失败与错误处理

发布于 2026-04-05 09:17:54 · 浏览 34 次 · 评论 0 条

JavaScript 网络问题:fetch 请求失败与错误处理


使用 fetch 发起的请求看似简单,但很多开发者踩过一个共同的坑:请求明明失败了,代码却没有进入错误处理流程。这是因为 fetch 的设计理念与传统的 XMLHttpRequest 不同,它只有在网络完全不可达时才会触发 reject,而对于服务器返回的 404、500 等 HTTP 错误状态,fetch 会正常进入 then 链。本指南将帮你彻底掌握 fetch 的错误处理机制,并学会构建一套健壮的网络请求方案。


为什么 fetch 不会自动抛出错误

理解 fetch 的行为是解决问题的第一步。当服务器返回一个 HTTP 错误状态码(如 404 Not Found 或 500 Internal Server Error)时,fetch 请求本身是成功的——它成功完成了网络通信,只是服务器告诉它"你要的东西不存在"或"服务器内部出错"。

// 这段代码在遇到 404 时,catch 块不会执行
fetch('https://api.example.com/nonexistent')
  .then(response => {
    console.log('请求完成了');  // 会执行到这里
    console.log(response.status); // 404
  })
  .catch(error => {
    console.log('进入 catch'); // 不会执行
  });

fetch 只有在以下情况才会触发 reject网络层面的故障,比如 DNS 解析失败、服务器完全不可达、断网等。这种设计让开发者有更大的灵活性来决定什么才算"错误",但也意味着你必须手动检查 HTTP 状态码。


正确处理 HTTP 错误状态

检查 response.ok 属性

fetchResponse 对象提供了一个非常实用的属性 ok,它是一个布尔值。当 HTTP 状态码在 200-299 范围内时,oktrue;其他情况下都为 false。结合这个属性,你可以准确捕获服务器端的错误。

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      // 手动抛出一个错误,跳转到 catch 块
      throw new Error(`HTTP 错误!状态码: ${response.status}`);
    }
    return response.json(); // 只有状态正常才解析 JSON
  })
  .then(data => {
    console.log('数据获取成功:', data);
  })
  .catch(error => {
    console.log('请求失败:', error.message);
  });
```

这种方法将网络错误和业务逻辑错误统一到 `catch` 块中处理,让代码逻辑更加清晰。

### 使用 async/await 语法糖

如果你使用现代 JavaScript 语法,`async/await` 能让错误处理更加直观。配合 `try-catch` 块,你可以像处理同步代码一样处理网络请求。

```javascript
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`请求失败: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('捕获到错误:', error.message);
    // 在这里统一处理错误:显示提示、重试、记录日志等
    throw error; // 可以选择重新抛出,让调用方处理
  }
}

处理网络层面的故障

除了 HTTP 错误,你的代码还需要处理真正的网络问题。这些错误通常来自 fetchreject 路径,常见原因包括:用户断网、服务器宕机、DNS 解析失败、防火墙拦截等。

async function robustFetch(url, options = {}) {
  try {
    const response = await fetch(url, options);

    if (!response.ok) {
      throw new Error(`服务器返回错误: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    if (error.name === 'TypeError' && error.message.includes('fetch')) {
      // 这是网络错误的典型特征
      throw new Error('网络连接失败,请检查网络设置');
    }
    throw error; // 其他错误原样抛出
  }
}
```

---

## 处理请求超时

`fetch` 默认没有超时机制,如果服务器长时间不响应,请求会一直挂起。对于用户体验来说,添加超时控制是必要的。

### 方法一:使用 AbortController

现代浏览器支持 `AbortController`,它可以让你主动取消一个请求。结合 `setTimeout`,你可以实现超时功能。

```javascript
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    clearTimeout(timeoutId);

    if (error.name === 'AbortError') {
      throw new Error('请求超时,请稍后重试');
    }
    throw error;
  }
}

// 使用示例
fetchWithTimeout('https://api.example.com/data', {}, 3000)
  .then(data => console.log(data))
  .catch(error => console.error(error));

方法二:使用 Promise.race

如果你需要兼容不支持 AbortController 的旧环境,可以用 Promise.race 实现类似效果。

function fetchWithTimeoutLegacy(url, timeout = 5000) {
  const racePromise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('请求超时')), timeout);
  });

  const fetchPromise = fetch(url).then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  });
  
  return Promise.race([fetchPromise, racePromise]);
}
```

---

## 实现自动重试机制

网络请求偶尔会失败,特别是面对不稳定的网络环境时。实现自动重试机制可以显著提升应用的可靠性。

```javascript
async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      return await response.json();
    } catch (error) {
      console.log(`第 ${attempt} 次尝试失败: ${error.message}`);

      if (attempt === maxRetries) {
        throw new Error(`已重试 ${maxRetries} 次,仍未成功`);
      }
      
      // 等待一段时间后再重试,使用指数退避避免雪崩
      await new Promise(resolve => setTimeout(resolve, delay * attempt));
    }
  }
}
```

这段代码实现了几个关键点:**每次重试前等待一段时间**,并且**等待时间逐渐增加**(指数退避),这样可以避免在服务器压力大时造成更大冲击。

---

## 封装一个生产级请求函数

将上面的技巧组合起来,你可以封装一个功能完善的请求函数。

```javascript
const request = {
  async get(url, options = {}) {
    return this._fetch(url, { ...options, method: 'GET' });
  },
  
  async post(url, data, options = {}) {
    return this._fetch(url, {
      ...options,
      method: 'POST',
      headers: { 'Content-Type': 'application/json', ...options.headers },
      body: JSON.stringify(data)
    });
  },
  
  async _fetch(url, options) {
    const config = {
      timeout: 10000,
      retries: 2,
      ...options
    };
    
    const fetchPromise = this._doFetch(url, config);
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error('请求超时')), config.timeout);
    });
    
    return Promise.race([fetchPromise, timeoutPromise]);
  },
  
  async _doFetch(url, config) {
    for (let attempt = 1; attempt <= config.retries; attempt++) {
      try {
        const response = await fetch(url, config);
        
        if (!response.ok) {
          throw new Error(`${response.status} ${response.statusText}`);
        }

        // 根据 Content-Type 判断是否需要解析 JSON
        const contentType = response.headers.get('content-type');
        if (contentType && contentType.includes('application/json')) {
          return await response.json();
        }

        return await response.text();
      } catch (error) {
        if (attempt === config.retries) throw error;
        await new Promise(r => setTimeout(r, 1000 * attempt));
      }
    }
  }
};

// 使用示例
try {
  const users = await request.get('https://api.example.com/users');
  console.log(users);
} catch (error) {
  console.error('获取用户列表失败:', error.message);
}

这个封装提供了:统一的 API 接口自动超时控制自动重试机制智能的响应解析,让你的业务代码专注于数据处理,而不必重复写错误处理逻辑。


常见的失败场景与排查方法

CORS 跨域错误

浏览器出于安全考虑,会阻止跨域请求。如果你的前端代码和 API 不在同一个域名下,服务器必须正确设置 Access-Control-Allow-Origin 头。浏览器的控制台会显示类似 "Access to fetch at 'https://api.example.com/' from origin 'http://localhost:3000' has been blocked by CORS policy" 的错误信息。

排查方法是确认服务器返回了正确的 CORS 响应头,并且前端请求中没有携带浏览器不允许的请求头(如自定义的 Authorization 头需要在预检请求中得到允许)。

重定向导致的意外结果

fetch 默认会跟随重定向。如果服务器返回 301 或 302 重定向,fetch 会自动跳转到新地址。这可能导致你获取到的数据来自一个完全不同的 URL,却浑然不知。如果怀疑有重定向问题,可以检查 response.url 是否与请求的 URL 一致。

JSON 解析失败

服务器返回的响应可能不是有效的 JSON,但你的代码假设它是。这种情况会抛出 "Unexpected token" 错误。解决方案是在解析 JSON 前先检查响应状态,并考虑使用 try-catch 包裹 response.json() 调用。


调试技巧

当 fetch 请求失败时,快速定位问题至关重要。打开浏览器开发者工具,切换到 Network 面板,找到对应的请求,查看 Status 列的状态码。如果状态码是 pending,说明请求还在进行中;如果是 (failed),说明是网络层面的问题;如果显示红色状态码(如 404、500),则是 HTTP 错误。

同时检查 Request Headers 和 Response Headers,确认请求头是否正确设置,响应头是否符合预期。对于 POST 请求,还要确认发送的 Request Payload 格式是否正确。


总结

fetch 的错误处理需要开发者主动检查 response.ok 属性,手动区分网络错误和 HTTP 错误。通过封装超时控制、重试机制和统一的错误处理逻辑,你可以构建出稳定可靠的网络请求层。记住:网络请求永远是不可靠的,你要做的不是假设它会成功,而是为失败做好准备

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文