先记住这个答案
fetch 的 Promise 只在网络层失败时 reject,比如 DNS 解析失败、TCP 连接中断、CORS 被拦截、请求被 AbortController 取消。只要服务器返回了响应,哪怕是 404 或 500,Promise 都会 resolve 一个 Response 对象。正确做法是拿到响应后检查 response.ok(status 在 200–299 时为 true)或 response.status,不满足就手动 throw,把 HTTP 错误转成可捕获的异常,再统一在 catch 里处理。
- fetch 只在网络层失败时 reject
- 404/500 会正常 resolve Response
- 用 response.ok 判断后手动 throw
fetch 的 reject 语义只覆盖网络层
fetch 的设计把「请求是否送达并收到响应」和「响应的业务成败」分成两层。Promise 的 resolve 只承诺浏览器拿到了响应头和状态行,此时服务器明确给出了答复,传输层面是成功的。404 表示资源不存在、500 表示服务器内部错误,这些都是应用层语义,fetch 不替开发者裁决它们算不算失败。
真正触发 reject 的情况很有限:URL scheme 非法、DNS 解析失败、连接被拒绝或重置、CORS 预检不通过、调用 AbortController 取消请求。这些情况共同点是没有拿到可用的 Response。判断时可用 response.ok(等价于 status 在 200–299)或直接比较 response.status,然后 throw 一个带状态码的错误。
function checkResponse(status) {
const ok = status >= 200 && status <= 299;
if (!ok) {
const err = new Error('HTTP ' + status);
err.status = status;
throw err;
}
return 'resolved';
}
function handle(status) {
try {
const r = checkResponse(status);
return JSON.stringify({ status: status, result: r });
} catch (e) {
return JSON.stringify({ status: status, result: 'thrown', code: e.status });
}
}
console.log(handle(200));
console.log(handle(404));
console.log(handle(500));查看输出与解释
{"status":200,"result":"resolved"}
{"status":404,"result":"thrown","code":404}
{"status":500,"result":"thrown","code":500}在纯 JavaScript 环境可运行,演示 response.ok 的判定区间和手动抛错逻辑,与浏览器中检查 Response 的写法一致。
详情页请求不存在商品时静默成功的坑
电商详情页用 fetch 请求 /api/products/123,商品已下架时后端返回 404 和一段 JSON 错误信息。代码写成 fetch(url).then(r => r.json()).then(render),于是 404 的响应被当成正常数据走渲染流程,页面渲染出 undefined 的商品名和价格,没有任何报错提示,线上监控也抓不到异常。
修复方式是在 then 链最前面加判断:if (!response.ok) throw new Error('HTTP ' + response.status),并在 catch 中按 status 区分处理——404 跳转到「商品不存在」页,5xx 显示重试按钮。这样 HTTP 错误从静默失败变成显式分支,监控也能采集到具体状态码。
这样处理有效的原因是它把两层语义重新分开:网络异常和 HTTP 错误都汇聚到 catch,但 catch 里可以通过 error.status 是否存在区分来源。没有 status 的是网络层失败或主动取消,有 status 的是服务器返回的错误,两者给用户的提示文案和重试策略应当不同。
response.ok 判断的失效边界
response.ok 只覆盖 200–299,但有些接口约定 304 也算可用缓存结果,此时 ok 为 false 会把可用响应误判成失败;反过来,也有后端把业务失败包在 200 里,靠 body 中的 code 字段表达,此时 ok 为 true 会把失败误判成成功。这两种情况下只看 ok 都会误判,需要按接口契约补充状态码白名单或 body 层的判断。
另一个边界是 body 解析阶段:即使 ok 为 true,response.json() 也可能因返回了 HTML 错误页或空 body 而 reject,这个 reject 来自解析而非请求。健壮写法是把状态检查和解析错误分别捕获,代价是代码更长,但能给出精确的错误定位,避免把网关的 502 HTML 页误报成「数据格式错误」。
容易答错的地方
- 以为非 2xx 状态会自动进 catch
- fetch 对 404、500 照样 resolve,直接链式调用 r.json() 会把错误响应当正常数据处理。必须在 then 里先检查 response.ok 并手动 throw,错误才会进入 catch 分支。
- 用 try/catch 包住 await fetch 就觉得万无一失
- try/catch 只能捕获 reject 和同步抛错,await fetch 拿到 404 响应时不会抛任何东西。如果不显式检查 ok 并 throw,catch 块对 HTTP 错误永远不会执行。
面试官还会怎么问?
catch 到的错误怎么区分是网络失败还是被取消的?
被取消时 reject 的是 name 为 AbortError 的 DOMException,网络失败在 Chrome 中是 TypeError: Failed to fetch。判断 e.name === 'AbortError' 即可区分,取消通常不需要提示用户,网络失败则需要。
response.ok 和直接判断 status === 200 有什么区别?
ok 覆盖 200–299 整个区间,包括 201、204 等成功状态;status === 200 会把 204 No Content 误判为失败。除非接口契约明确只有 200 算成功,否则优先用 ok。
可以封装一个自动对 HTTP 错误 reject 的 fetch 吗?
可以,包一层函数检查 ok 并 throw 即可,团队内统一使用能减少遗漏。但要注意保留原始 Response 和 status 供上层判断,且不要吞掉 AbortError 的语义,否则取消逻辑会被误当成请求失败。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。