请求错误分类:网络/超时/取消/HTTP 状态/业务码
阶段 7.1|错误分类:网络 / 超时 / 取消 / HTTP 状态 / 业务码
一、为什么要先分类
不同错误的处置完全相反:
| 错误 | 处置 |
|---|---|
| 取消 | 静默(弹提示就是 bug) |
| 超时/网络 | 提示 + 可重试 |
| 401 | 提示 + 跳登录 |
| 403/404 | 提示 + 不重试 |
| 5xx | 提示 + 可重试 |
| 业务码 | 提示后端文案 + 不重试 |
混为一谈的结果:切页面弹一堆"失败"、超时不重试、401 后用户点啥都没反应。
二、五层错误模型(从底到上)
L4 业务码错误 HTTP 200,但 { code: '5001', errorMsg: '余额不足' }
L3 HTTP 状态错误 401 / 403 / 404 / 500 / 502 ...
L2 超时 超过设定时间未响应
L1 网络层错误 断网 / DNS / CORS / 证书 / 连接被拒
L0 主动取消 自己 abort 的(不是错误!)
判断顺序:从上往下排除(先看是不是自己取消的,再看有没有 response)。
三、逐层拆解
L0 主动取消(不是错误)
| 识别 | |
|---|---|
| fetch | err.name === 'AbortError' / signal.aborted |
| axios | err.code === 'ERR_CANCELED' / axios.isCancel(err) |
处置:静默,return,什么都不做。
L1 网络层错误
成因:断网、DNS 解析失败、CORS 被拒、SSL 证书错误、后端服务没起、连接被重置。
| 识别 | |
|---|---|
| fetch | reject,TypeError: Failed to fetch,没有 response |
| axios | err.code === 'ERR_NETWORK' 或 !err.response |
| XHR | 触发 onerror,status === 0 |
控制台通常伴随:
CORS policy、net::ERR_...
处置:提示"网络异常",可重试。
L2 超时
| 识别 | |
|---|---|
AbortSignal.timeout() |
err.name === 'TimeoutError' |
axios timeout |
err.code === 'ECONNABORTED'(需开 transitional.clarifyTimeoutError) |
处置:提示"请求超时",可重试(但要注意幂等,见 7.4)。
⚠️ 超时 ≠ 服务端没收到。POST 超时重试可能重复提交。
L3 HTTP 状态错误
| 状态码 | 含义 | 重试 | 处置 |
|---|---|---|---|
| 400 | 请求参数错误 | ❌ | 提示后端 message |
| 401 | 未认证 / token 失效 | ❌ | 跳登录(阶段 8:无感刷新) |
| 403 | 已登录但无权限 | ❌ | 提示"无权限" |
| 404 | 资源不存在 | ❌ | 提示 |
| 409 | 冲突(重复提交/版本冲突) | ❌ | 提示 |
| 429 | 请求过于频繁 | ⚠️ 延迟后重试 | 提示 + 退避 |
| 500 | 服务端内部错误 | ✅ | 提示"服务异常" |
| 502/503 | 网关错误 / 服务不可用 | ✅ | 提示 + 重试 |
| 504 | 网关超时 | ✅ | 提示 + 重试 |
识别:err.response.status(axios)/ res.status(fetch 需自己 res.ok 判断)。
L4 业务码错误(最容易漏)⭐
HTTP 200,但业务失败 —— axios 和 fetch 都不会把它当错误:
{ "code": "5001", "errorMsg": "当前期间已结账,无法调整", "data": null }
必须在响应拦截器的成功分支里判断 + reject:
if (raw.code !== '0' && raw.errorCode !== '0') {
message.error(raw.errorMsg)
return Promise.reject(raw) // ⭐ 转成错误
}
L5 解析/数据错误(补充)
await res.json() // 返回了 HTML 错误页 → SyntaxError
data.records.map(...) // 字段缺失 → TypeError
处置:捕获 + 上报,通常不重试。
四、识别方式对照表
| 层 | fetch | axios | XHR |
|---|---|---|---|
| 取消 | err.name === 'AbortError' |
code === 'ERR_CANCELED' |
onabort、status 0 |
| 网络 | TypeError: Failed to fetch |
!err.response / ERR_NETWORK |
onerror、status 0 |
| 超时 | err.name === 'TimeoutError' |
code === 'ECONNABORTED' |
ontimeout |
| HTTP | 手动 !res.ok + res.status |
err.response.status |
status |
| 业务码 | 手动判 data.code |
手动判(拦截器统一做) | 手动判 |
五、归一化:把任意错误变成一种结构 ⭐
export type ErrorKind = 'cancel' | 'network' | 'timeout' | 'http' | 'business' | 'parse' | 'unknown'
export class ApiError extends Error {
readonly kind: ErrorKind
readonly status?: number
readonly code?: string
readonly raw?: unknown
constructor(o: { kind: ErrorKind, message: string, status?: number, code?: string, raw?: unknown }) {
super(o.message)
this.name = 'ApiError'
this.kind = o.kind
this.status = o.status
this.code = o.code
this.raw = o.raw
}
/** 是否可安全重试(幂等前提下) */
get retryable() {
return this.kind === 'network' || this.kind === 'timeout'
|| (this.kind === 'http' && !!this.status && this.status >= 500)
|| (this.kind === 'http' && this.status === 429)
}
}
/** 把 axios/fetch 的任意错误归一化成 ApiError */
export function classifyError(err: unknown): ApiError {
if (err instanceof ApiError)
return err
const e = err as any
// ① 取消
if (e?.name === 'AbortError' || e?.name === 'CanceledError' || e?.code === 'ERR_CANCELED')
return new ApiError({ kind: 'cancel', message: '已取消', raw: err })
// ② 超时
if (e?.name === 'TimeoutError' || e?.code === 'ECONNABORTED')
return new ApiError({ kind: 'timeout', message: '请求超时', raw: err })
// ③ 网络层(没有 response)
if (!e?.response)
return new ApiError({ kind: 'network', message: '网络异常', raw: err })
// ④ HTTP 状态
const status = e.response.status
const msg = e.response.data?.errorMsg ?? `请求失败(${status})`
return new ApiError({ kind: 'http', status, message: msg, raw: err })
}
业务层就能这样写:
catch (e) {
const err = classifyError(e)
if (err.kind === 'cancel') return // 静默
if (err.retryable) showRetryButton() // 可重试
if (err.status === 401) goLogin()
}
六、五个坑 ⚠️
- 把业务码失败当成功 —— 最致命,页面拿着
null渲染。 - CORS 错误拿不到 status ——
status是 0,只能归到"网络层"。 - 超时不代表服务端没执行 —— POST 重试前必须确认幂等(7.4)。
- 401 与 403 混为一谈 —— 401 是"你是谁"(去登录),403 是"你没权限"(别跳登录)。
- 429 要退避重试 —— 直接重试会加重限流。
✅ 自检
Q1:L0~L4 五层错误分别是什么?
取消 / 网络 / 超时 / HTTP 状态 / 业务码。
Q2:业务码错误为什么最容易被漏掉?
HTTP 状态码是 200,fetch 和 axios 都当成成功,必须在拦截器成功分支里判
code并 reject。
Q3:怎么区分"网络失败"和"HTTP 错误"?
看有没有
response:没 response = 网络层(status为 0);有 response = HTTP 状态错误。
Q4:哪些错误可以自动重试?
网络失败、超时、5xx、429(前提是幂等请求)。
Q5:401 和 403 的处理有什么不同?
401 = 未认证 → 跳登录/刷新 token;403 = 无权限 → 只提示,不能跳登录(会造成死循环)。

浙公网安备 33010602011771号