写前端代码这几年,我见过太多人在“异步请求”这个环节栽跟头。有时候是因为一个奇怪的跨域报错卡了半小时,有时候是因为 $.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();

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

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.ok 或 response.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 状态码(如 404、500)。一定要分清这两个。

另外,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.status 和 xhr.responseText 获取更多信息。

写在最后

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

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

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

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