请求错误分类:网络/超时/取消/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()
}

六、五个坑 ⚠️

  1. 把业务码失败当成功 —— 最致命,页面拿着 null 渲染。
  2. CORS 错误拿不到 status —— status 是 0,只能归到"网络层"。
  3. 超时不代表服务端没执行 —— POST 重试前必须确认幂等(7.4)。
  4. 401 与 403 混为一谈 —— 401 是"你是谁"(去登录),403 是"你没权限"(别跳登录)。
  5. 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 = 无权限 → 只提示,不能跳登录(会造成死循环)。

posted @ 2026-10-14 14:46  寅信  阅读(6)  评论(0)    收藏  举报