前端如何区分网络错误、HTTP 错误和业务错误?
简化版
前端请求失败要区分三层:网络错误是请求没拿到有效 HTTP 响应,例如断网、DNS、CORS 被拦、超时取消;HTTP 错误是拿到了响应但状态码非 2xx,例如 401、403、500;业务错误是 HTTP 可能为 200,但响应体里的业务码表示失败。区分这些错误能决定提示文案、重试策略、登录跳转和监控归因。
详细版
Fetch 有一个容易误解的点:HTTP 500 不会让 Promise reject,只有网络层失败才 reject。
const res = await fetch('/api/user')
if (!res.ok) throw new HttpError(res.status)
const body = await res.json()
if (body.code !== 0) throw new BizError(body.code, body.message)
| 类型 | 判断 | 示例 |
|---|---|---|
| 网络错误 | fetch reject | 断网、CORS、超时 |
| HTTP 错误 | !res.ok | 401、403、500 |
| 业务错误 | body code 非成功 | 库存不足、参数错误 |
错误分层清楚,请求封装才不会把所有失败都提示成“网络异常”。
完整版教学
一、为什么要分层
用户看到的都是“失败”,但系统处理方式完全不同。断网可以提示检查网络,401 要跳登录,403 是权限问题,500 要上报服务异常,业务错误要展示业务文案。
如果全部混成一个错误类型,用户体验和监控都会很差。
二、网络错误
网络错误通常表示没有拿到正常 HTTP 响应。Fetch 会 reject。
常见原因包括:
- 用户断网。
- DNS 失败。
- TLS 失败。
- CORS 不允许。
- 请求被 AbortController 取消。
- 浏览器策略拦截。
CORS 失败在前端也常表现得像网络错误,因为浏览器不会把被拦响应暴露给 JS。
三、HTTP 错误
HTTP 错误是服务端返回了状态码,但状态码表示请求失败。Fetch 不会因为 404 或 500 自动 reject。
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
这点是面试常见坑。
四、业务错误
很多接口使用统一响应:
{ "code": 10001, "message": "库存不足", "data": null }
HTTP 状态码可能仍是 200,但业务上失败。请求封装要解析业务 code。
五、不同错误如何处理
| 错误 | 处理 |
|---|---|
| 网络错误 | 提示网络/重试 |
| 超时 | 提示稍后重试,可重试 GET |
| 401 | 清登录态并跳登录 |
| 403 | 提示无权限 |
| 5xx | 服务异常,上报监控 |
| 业务错误 | 展示后端业务文案 |
不要把所有错误都弹 toast,否则用户会被打扰。
六、请求封装建议
可以定义错误类:
class HttpError extends Error {
constructor(public status: number, message = `HTTP ${status}`) {
super(message)
}
}
业务层捕获后根据类型处理。监控也能按类型统计。
七、常见误区与追问
- 误区:HTTP 500 会让 fetch 自动 reject。 Fetch 只有网络失败才 reject,HTTP 状态要自己判断。
- 误区:CORS 错误能读到服务端响应体。 浏览器会拦截响应,前端通常拿不到详细内容。
- 误区:业务 code 失败就是网络失败。 它属于业务语义,提示和重试策略不同。
- 追问:401 和 403 区别? 401 偏未认证,403 偏已认证但无权限。
- 追问:超时算网络错误吗? 在前端封装里通常归为网络/取消类错误,但最好单独标记。
- 追问:为什么要自定义错误类? 方便业务层和监控按类型处理,而不是解析字符串。
八、加强记忆
请求错误记成“三层失败”:没响应是网络错误,有响应状态坏是 HTTP 错误,状态正常但 code 失败是业务错误。分清层次,提示、重试和监控才准确。