ajax 异常处理 try、catch、error、complete
'# AJAX 异常处理:try、catch、error、complete 的深度解析
一、背景与问题
在现代 Web 开发中,AJAX(Asynchronous JavaScript and XML)技术已成为实现动态交互的核心手段。然而,其异常处理机制始终是开发者面临的挑战。传统 AJAX 调用存在以下典型问题:
- 网络错误无法捕获:如断网、服务器宕机等场景
- 服务器错误未区分:4xx/5xx 状态码需手动识别
- 异步回调不统一:
onerror/oncomplete与try/catch策略冲突 - 超时处理缺失:未设置请求超时机制导致阻塞
- 异常链丢失:错误信息未携带上下文信息
本篇文章将深入解析 AJAX 异常处理的底层机制,结合实际开发场景,提供可复用的解决方案。
二、基本原理
AJAX 异常处理涉及两个核心机制:
1. 事件驱动模型
XMLHttpRequest 基于事件驱动模型,通过以下事件处理:
onload:请求成功完成onerror:网络错误(如断网)ontimeout:超时错误onprogress:传输进度
2. Promise 链式调用
Fetch API 基于 Promise 的异步处理机制,通过 .catch() 捕获错误,但存在以下特性:
- 网络错误(如 DNS 解析失败)不会触发
.catch() - 需要手动检查响应状态码(
response.ok)
3. 异步函数控制流
ES6 引入的 async/await 提供更清晰的同步式写法,但需要处理 Promise 链的异常传播。
三、环境准备
# 创建项目目录
mkdir ajax-exception-demo
cd ajax-exception-demo
# 初始化项目
npm init -y
npm install xmlhttprequest四、核心实现
1. 基础 AJAX 异常处理(XMLHttpRequest)
function fetchWithXHR(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 设置超时时间
xhr.timeout = 5000;
xhr.ontimeout = () => {
reject(new Error('Request timeout'));
};
xhr.onerror = () => {
reject(new Error('Network error'));
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText);
} else {
reject(new Error(`Server error: ${xhr.status}`));
}
};
xhr.open('GET', url, true);
xhr.send();
});
}
// 使用示例
fetchWithXHR('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));关键点解释:
timeout属性设置超时时间(单位:毫秒)onerror处理网络错误(如断网)onload需要手动判断 HTTP 状态码- 使用 Promise 封装实现异步控制
2. Fetch API 异常处理
async function fetchWithFetch(url) {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// 使用示例
fetchWithFetch('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));关键点解释:
fetch()返回的 Promise 仅在网络错误时触发.catch()(如 DNS 错误)- 需要手动检查
response.ok判断 HTTP 状态码 - 使用
try/catch捕获所有异常(包括网络错误和服务器错误)
3. async/await 异常处理
async function fetchWithAsync(url) {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Async error:', error);
throw error;
}
}
// 使用示例
fetchWithAsync('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));关键点解释:
- 使用
await等待 Promise 完成 try/catch捕获所有异常- 可结合
AbortController实现请求取消
五、完整案例
场景:用户登录验证系统
// login.js
async function login(username, password) {
const url = 'https://api.example.com/auth/login';
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.token) {
localStorage.setItem('token', data.token);
return true;
}
throw new Error('Invalid credentials');
} catch (error) {
console.error('Login error:', error);
return false;
}
}
// login.html
document.getElementById('login-form').addEventListener('submit', async function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const success = await login(username, password);
if (success) {
alert('Login successful!');
window.location.href = '/dashboard';
} else {
alert('Login failed. Check your credentials.');
}
});异常处理策略:
- 使用
try/catch捕获所有异常 - 区分 HTTP 状态码和业务错误
- 返回布尔值控制流程
- 本地存储 token 用于后续请求
六、源码解析
1. XMLHttpRequest 事件处理流程
// 源码片段(简化版)
XMLHttpRequest.prototype.open = function(method, url) {
this._method = method;
this._url = url;
this._timeout = 0; // 默认超时时间
this._onload = null;
this._onerror = null;
this._ontimeout = null;
};
XMLHttpRequest.prototype.send = function(data) {
// 模拟请求发送
setTimeout(() => {
if (this._onload) {
this._onload(); // 触发 onload 事件
}
}, 1000); // 模拟1秒后完成
};关键点:
- 事件处理函数需显式绑定
- 超时处理需要独立的定时器
- 状态码判断需手动实现
2. Fetch API 的 Promise 链
// 源码片段(简化版)
fetch(url, options) {
return new Promise((resolve, reject) => {
const request = new Request(url, options);
const xhr = new XMLHttpRequest();
xhr.open(request.method, request.url);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText);
} else {
reject(new Error(`HTTP error! status: ${xhr.status}`));
}
}
};
xhr.onerror = () => {
reject(new Error('Network error'));
};
xhr.ontimeout = () => {
reject(new Error('Timeout'));
};
xhr.send();
});
}关键点:
- 使用底层 XMLHttpRequest 实现
- 需要手动处理所有错误场景
- Promise 链式调用需要正确处理 reject
七、进阶使用
1. 统一异常处理中间件
// utils.js
function createFetchMiddleware() {
return (url, options) => {
return fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
})
.catch(error => {
console.error('Middleware error:', error);
throw error;
});
};
}
// 使用示例
const fetchWithMiddleware = createFetchMiddleware();
fetchWithMiddleware('https://api.example.com/data')
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));2. 请求取消机制
// 取消请求示例
async function fetchWithCancel(url) {
const controller = new AbortController();
try {
const response = await fetch(url, {
method: 'GET',
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// 取消请求
const controller = new AbortController();
setTimeout(() => controller.abort(), 3000);3. 错误日志记录
// 错误日志记录中间件
function logErrors() {
return (url, options) => {
return fetch(url, options)
.catch(error => {
console.error('Error occurred:', error);
return Promise.reject(error);
});
};
}八、性能与工程实践
1. 超时设置策略
// 动态超时设置
function getTimeout(url) {
if (url.includes('/login')) {
return 2000; // 登录请求更短超时
}
return 5000; // 默认超时
}最佳实践:
- 短请求设置更短超时
- 长请求设置更长超时
- 超时后立即终止请求
2. 错误重试机制
function retryFetch(url, maxRetries = 3) {
return new Promise((resolve, reject) => {
let retries = maxRetries;
const attempt = () => {
fetch(url)
.then(response => {
if (!response.ok) {
if (retries > 0) {
retries--;
setTimeout(attempt, 1000);
} else {
reject(new Error('Max retries exceeded'));
}
} else {
resolve(response);
}
})
.catch(error => {
if (retries > 0) {
retries--;
setTimeout(attempt, 1000);
} else {
reject(error);
}
});
};
attempt();
});
}注意事项:
- 避免无限重试
- 需要处理服务端熔断机制
- 不适合敏感操作(如支付)
3. 异常处理性能优化
- 避免重复处理:使用
Promise.race()跳过已处理的请求 - 批量处理:对多个 AJAX 请求进行聚合处理
- 缓存异常信息:记录常见错误代码避免重复处理
- 异步日志:避免阻塞主线程
九、常见问题与踩坑
1. 常见错误场景
| 场景 | 错误 | 解决方案 |
|---|---|---|
忽略 onerror | 未捕获网络错误 | 绑定 onerror 事件 |
| 错误判断不严谨 | 未检查 HTTP 状态码 | 使用 response.ok 判断 |
| 未处理超时 | 请求阻塞 | 设置 timeout 属性 |
未处理 onload | 未处理成功响应 | 绑定 onload 事件 |
未处理 onprogress | 未处理传输进度 | 监听 onprogress 事件 |
2. 常见错误示例
错误示例:
fetch('https://api.example.com/data')
.then(response => response.json())
.catch(error => console.error('Error:', error));错误分析:
- 未检查
response.ok导致误判 - 网络错误不会触发
.catch()(如 DNS 解析失败)
改进方案:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => console.error('Error:', error));3. 安全风险
| 风险 | 防范措施 |
|---|---|
| 未验证响应类型 | 设置 Content-Type 检查 |
| 未处理异常链 | 记录完整错误信息 |
| 跨域请求安全 | 配置 CORS 策略 |
| 敏感数据泄露 | 使用 HTTPS 加密传输 |
十、最佳实践
1. 推荐方案
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 简单场景 | XMLHttpRequest | 原生支持,控制精细 |
| 现代项目 | Fetch API + async/await | 代码简洁,可读性强 |
| 高级控制 | Fetch API + AbortController | 支持请求取消 |
| 全局控制 | 中间件模式 | 统一异常处理逻辑 |
2. 实践建议
- 统一错误码:定义标准错误码(如 4001-4999)
- 错误信息本地化:根据用户语言显示错误提示
- 日志记录:记录错误堆栈信息
- 重试策略:对特定错误码进行重试
- 熔断机制:对连续失败请求进行熔断处理
- 性能监控:记录请求耗时和错误率
十一、总结
AJAX 异常处理是构建健壮 Web 应用的关键环节。本文深入探讨了不同技术方案的原理和实现,提供了多个可复用的代码示例。在实际开发中,应根据具体场景选择合适的异常处理机制:
- 使用
XMLHttpRequest时,需要同时处理onerror/oncomplete和状态码判断 - 使用
Fetch API时,需注意网络错误和服务器错误的区分 - 使用
async/await时,应配合try/catch实现完整的异常链处理
在实际项目中,建议采用中间件模式统一处理异常,同时结合重试机制、熔断机制等高级特性构建健壮的系统。记住:异常处理不是简单的错误捕获,而是系统稳定性的重要保障。
评论已关闭