写前端代码这几年,我见过太多人在“异步请求”这个环节栽跟头。有时候是因为一个奇怪的跨域报错卡了半小时,有时候是因为 $.ajax 里的 async: false 导致了严重的性能问题,还有时候是因为搞不清 fetch 返回的 Promise 状态。

其实吧,这三个东西本质干的是同一件事:让浏览器在后台偷偷去服务器拿数据,别让页面卡死。 但它们的写法、陷阱和排查思路完全不同。今天咱就把这三件“老朋友”拉出来溜溜,顺便把那些让人头疼的报错一一消灭。

XMLHttpRequest:那个“老古董”依然很能打

先说 XMLHttpRequest(简称 xhr)。这东西诞生于1999年,比 JavaScript 本身都老不了多少。虽然现在大家都追捧 fetch,但在很多老项目里,甚至一些对兼容性要求极高的场景下,xhr 依然是主力。

基本用法长这样

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true); // 第三个参数 true 表示异步

xhr.onreadystatechange = function () {
    // readyState 有 5 个状态
    if (xhr.readyState === 4) { // 4 表示请求完成
        if (xhr.status === 200) {
            console.log('成功啦:', JSON.parse(xhr.responseText));
        } else {
            console.error('请求失败,状态码:', xhr.status);
        }
    }
};

xhr.send();

你看,这代码是不是有点啰嗦?readyStatestatusresponseText… 一堆东西要记。但好处是,它能给你极细粒度的控制。比如,你想监听上传进度:

xhr.upload.onprogress = function(event) {
    if (event.lengthComputable) {
        var percentComplete = (event.loaded / event.total) * 100;
        console.log('上传进度:' + percentComplete + '%');
    }
};

fetch 做不到这个,这是 xhr 的一大优势。

常见报错:别被 onerror 骗了

很多新手用 xhr 时,会写这样的代码:

xhr.onerror = function() {
    console.log('出错了');
};

然后发现,哪怕服务器返回 404,这个 onerror 也不会触发。为什么?因为 xhr 的 onerror 只在网络请求完全失败时才触发,比如断网、DNS 解析失败、跨域问题被浏览器拦截等。如果服务器正常响应了(哪怕是 404、500),它都算“成功”,只是状态码不对而已。

所以,正确的姿势是:

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        console.log('成功');
    } else {
        console.error('HTTP 错误:', xhr.status);
    }
};

xhr.onerror = function() {
    console.error('网络错误,可能断网或跨域被拦截');
};

记住:状态码 2xx 才是真的成功,别只依赖 onload 就以为万事大吉。

Fetch:现代浏览器的新宠

2015 年左右,fetch 出现了。它的理念很简单:用 Promise 来简化异步编程。语法优雅,链式调用,看起来比 xhr 高级多了。

基本用法

fetch('https://api.example.com/data')
    .then(response => {
        if (!response.ok) {
            throw new Error('HTTP 错误:' + response.status);
        }
        return response.json();
    })
    .then(data => {
        console.log('成功啦:', data);
    })
    .catch(error => {
        console.error('出错了:', error);
    });

看起来是不是清爽很多?但这里有个巨大的坑,几乎所有开发者都踩过。

最大的坑:fetch 不会 rejection 掉 HTTP 错误

看上面那段代码,我特意加了 if (!response.ok) 的判断。为什么?因为 fetch 默认情况下,即使服务器返回 404 或 500,Promise 依然是 resolved 状态,它不会自动 throw error。只有网络完全失败(比如断网)时,才会进入 catch

这就导致很多人写这样的代码:

fetch('/api/data')
    .then(res => res.json())
    .then(data => console.log(data));

结果服务器返回 400,res.json() 成功执行了,只是拿到的数据可能是个错误提示,然后你的页面就崩了,而你还不知道哪儿出了问题。

所以,必须手动检查 response.okresponse.status

发送 POST 请求

fetch('/api/user', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: '张三', age: 25 })
})
.then(res => {
    if (!res.ok) throw new Error('提交失败');
    return res.json();
})
.then(data => console.log('提交成功', data))
.catch(err => console.error(err));

注意 headers 要自己设置 Content-Type,不像 jQuery 那样自动序列化。这是 fetch 的一个小麻烦。

取消请求

fetch 用 AbortController 来取消:

const controller = new AbortController();
const signal = controller.signal;

fetch('/api/data', { signal })
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(err => {
        if (err.name === 'AbortError') {
            console.log('请求被取消了');
        } else {
            console.error('其他错误', err);
        }
    });

// 3秒后取消
setTimeout(() => controller.abort(), 3000);

xhr 也能取消(用 xhr.abort()),但 fetch 的 AbortController 更符合现代 API 设计。

jQuery $.ajax:曾经的王者

在 fetch 出现之前,$.ajax 几乎是前端开发的标配。它的优点是把复杂的 xhr 封装得非常友好,自动处理很多细节,写起来省心。

基本用法

$.ajax({
    url: 'https://api.example.com/data',
    type: 'GET',
    dataType: 'json',
    success: function(data) {
        console.log('成功:', data);
    },
    error: function(xhr, status, error) {
        console.error('失败:', status, error);
    }
});

自动处理 JSON

jQuery 最香的一点是 dataType: 'json',它会自动帮你把响应解析成 JSON 对象,不用你手动 JSON.parse()。而且,如果服务器返回的是非 JSON 格式,jQuery 会报错,这反而有助于早期发现问题。

发送请求

$.ajax({
    url: '/api/user',
    type: 'POST',
    data: { name: '张三', age: 25 },
    contentType: 'application/x-www-form-urlencoded',
    success: function(data) {
        console.log('成功', data);
    },
    error: function(xhr, status, err) {
        console.error('失败', status, err);
    }
});

注意,jQuery 默认用 application/x-www-form-urlencoded 格式发送数据,这和 fetch 的默认行为不同。如果你想用 JSON,需要手动设置:

$.ajax({
    url: '/api/user',
    type: 'POST',
    contentType: 'application/json',
    data: JSON.stringify({ name: '张三', age: 25 }),
    success: function(data) { ... },
    error: function(xhr, status, err) { ... }
});

全局错误处理

jQuery 有个很实用的功能:$.ajaxError()。它可以监听整个页面的所有 ajax 错误:

$(document).ajaxError(function(event, xhr, settings, thrownError) {
    console.log('某个请求出错了', settings.url, thrownError);
});

这在大型项目里非常有用,可以统一处理错误提示,不需要每个请求都写 error 回调。

常见报错:statusText 和 thrownError

很多人用 error 回调时,会这样写:

error: function(xhr, status, error) {
    console.log(status); // 可能是 "error" 或 "timeout"
    console.log(error); // 可能是 "" 或异常消息
}

注意,status 是 jQuery 自定义的状态字符串(如 "error""timeout""abort"),而 xhr.status 才是真正的 HTTP 状态码(如 404500)。一定要分清这两个

另外,thrownError 这个参数在很多教程里被忽略了。它其实是浏览器抛出的异常信息,在跨域问题时特别有用:

error: function(xhr, status, thrownError) {
    if (xhr.status === 0) {
        console.log('可能是跨域问题或网络断开');
        console.log(thrownError); // 这里可能有具体信息
    }
}

三者对比:我该用哪个?

好了,代码看完了,咱来聊聊怎么选。

特性 XMLHttpRequest Fetch jQuery $.ajax
语法简洁度 差(回调地狱) 好(Promise 链式) 好(配置对象)
错误处理 手动判断 status 需手动检查 ok 自动处理
取消请求 xhr.abort() AbortController jqXHR.abort()
上传进度 支持 不支持 支持(通过 xhr)
浏览器兼容 所有浏览器 现代浏览器(IE 不支持) 所有浏览器
自动 JSON 解析 有(dataType
代码体积 无依赖 无依赖 需引入 jQuery
学习曲线 陡峭 平缓 平缓

我的建议

如果你在维护老项目,或者需要支持 IE:继续用 jQuery $.ajax 或原生命名封装的 xhr。没必要强行改成 fetch,增加维护成本。

如果你在做新项目,且不需要支持 IE:优先用 fetch。它是浏览器原生支持的,零依赖,语法现代,和 Promise、async/await 搭配使用非常优雅。

如果你想兼顾兼容性和现代语法:可以用 axios 库。它底层基于 fetch(或 xhr),但提供了更统一的 API,自动 JSON 解析,拦截器等功能,很多现代项目都在用。

实际案例:用 async/await 重写 fetch

现代 JavaScript 推荐使用 async/await,它让异步代码看起来像同步代码,可读性大幅提升:

async function getUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('获取用户数据失败:', error);
        throw error; // 重新抛出,让调用者处理
    }
}

// 使用
getUserData(123)
    .then(user => console.log('用户信息:', user))
    .catch(err => console.error('错误:', err));

或者直接用顶级 await(在模块中):

// 在 ES module 中
const user = await getUserData(123);
console.log(user);

常见报错排查清单

最后,我把大家最常遇到的报错和排查思路整理一下,收藏起来备用:

1. CORS 跨域错误

现象:控制台报 Access-Control-Allow-Origin 错误,请求被浏览器拦截。

排查

  • 确认后端是否设置了正确的 CORS 头:Access-Control-Allow-Origin: * 或具体域名
  • 检查请求方法是否是简单请求(GET/POST/HEAD,且 Content-Type 是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)
  • 如果不是简单请求,浏览器会先发 OPTIONS 预检请求,确认后端支持

解决:后端配置 CORS,或使用代理服务器(开发环境)。

2. 404 Not Found

现象:请求返回 404,数据拿不到。

排查

  • 检查 URL 是否正确,注意大小写和路径分隔符
  • 确认服务器端路由是否配置正确
  • 如果是相对路径,检查基准路径是否正确

解决:修正 URL 或后端路由。

3. 401 Unauthorized / 403 Forbidden

现象:需要登录才能访问,但没传 token 或 token 无效。

排查

  • 检查请求头是否带了 Authorization 字段
  • 确认 token 是否过期
  • 检查 token 格式是否正确(如 Bearer <token>

解决:补充认证信息,或刷新 token。

4. 500 Internal Server Error

现象:服务器内部出错。

排查

  • 查看服务器日志,定位具体错误
  • 检查请求参数是否正确,有没有缺少必填字段
  • 确认服务器环境是否正常

解决:修复后端代码或调整请求参数。

5. TypeError: Failed to fetch

现象:网络完全失败,Promise 被 reject。

排查

  • 检查网络连接是否正常
  • 确认 URL 格式是否正确
  • 检查是否被浏览器扩展拦截(如广告拦截器)
  • 如果是 HTTP 链接,确认是否被混合内容策略阻止(HTTPS 页面不能请求 HTTP 资源)

解决:修复网络问题,或使用 HTTPS。

6. jQuery 中 statusText 为空

现象error 回调中 statusText 是空字符串。

排查

  • 这通常发生在跨域错误或网络断开时
  • jQuery 的 statusText 在某些情况下会被浏览器忽略

解决:改用 xhr.statusxhr.responseText 获取更多信息。

写在最后

异步请求这块技术,其实没有绝对的好坏,只有适不适合。xhr 虽然啰嗦,但在需要精细控制的场景下依然不可替代;fetch 简洁现代,是未来的方向;jQuery ajax 虽然老了点,但在遗留项目里依然稳定可靠。

作为开发者,我的建议是:熟悉这三种写法,知道它们各自的优缺点,然后根据项目需求灵活选择。别盲目追新,也别固步自封。

如果你在排查问题时卡住了,不妨静下心来,对照上面的清单一步步检查。大多数时候,问题就出在 URL 写错、跨域没配好、或者忘了检查 response.ok 这种小细节上。

希望这篇指南能帮到你。如果还有疑问,欢迎随时交流——毕竟,代码是写给人看的,顺便让机器执行,对吧?