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 属性
fetch 的 Response 对象提供了一个非常实用的属性 ok,它是一个布尔值。当 HTTP 状态码在 200-299 范围内时,ok 为 true;其他情况下都为 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 错误,你的代码还需要处理真正的网络问题。这些错误通常来自 fetch 的 reject 路径,常见原因包括:用户断网、服务器宕机、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 错误。通过封装超时控制、重试机制和统一的错误处理逻辑,你可以构建出稳定可靠的网络请求层。记住:网络请求永远是不可靠的,你要做的不是假设它会成功,而是为失败做好准备。

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