API错误码设计详解
互联网前后端与微服务协作里,错误怎么表达直接影响联调效率、监控质量和用户体验。成熟团队通常遵守一条分工:
HTTP 状态码 描述传输与协议层;业务
code描述应用语义;统一 JSON 信封让前端、网关、监控都能机器处理。
本文面向 全栈与架构 读者:不限定某一语言框架,覆盖单体到微服务、从设计原则到中心化治理与业界方案。适用于 REST / JSON、BFF / 网关、gRPC / Thrift 等常见形态,语言栈可替换。
速览
- 成功 :本文约定
code === 0(国内常见);云厂商 API 亦常见 HTTP 200 且无业务 code 或code: "OK"------团队按历史包袱选型即可。- 业务失败 :常见 HTTP 200 + code≠0;鉴权/限流/下游不可用用对应 HTTP 状态码。
- 前端 :用
code分支 ,不要用message字符串匹配。- 微服务 :错误要 可追溯、可归属、不吞下游语义 ;
traceId标配。- 治理 :错误码是 API 契约,宜 YAML/Proto 中心化 + CI 生成常量。
目录
- [1. 两层错误:HTTP 与业务码](#1. 两层错误:HTTP 与业务码)
- [2. 设计原则](#2. 设计原则)
- [3. 统一响应结构](#3. 统一响应结构)
- [4. 错误码如何编号](#4. 错误码如何编号)
- [5. 后端:全局异常与分层](#5. 后端:全局异常与分层)
- [6. 前端:拦截器与分支策略](#6. 前端:拦截器与分支策略)
- [7. 两种流派与生产折中](#7. 两种流派与生产折中)
- [8. 微服务:挑战与传递规范](#8. 微服务:挑战与传递规范)
- [9. 中心化错误码治理](#9. 中心化错误码治理)
- [10. 与 OpenAPI、监控联动](#10. 与 OpenAPI、监控联动)
- [11. 业界方案速览](#11. 业界方案速览)
- [12. 反模式](#12. 反模式)
- [13. 落地阶段建议](#13. 落地阶段建议)
- [14. 名词速查卡](#14. 名词速查卡)
1. 两层错误:HTTP 与业务码
| 层次 | 谁关心 | 典型值 | 作用 |
|---|---|---|---|
| HTTP Status | 浏览器、CDN、网关、爬虫 | 200、400、401、403、404、429、502、503 | 协议与基础设施语义 |
业务 code |
前端逻辑、产品文案、业务监控 | 0 成功;20002 密码错误 |
具体业务分支 |
#mermaid-svg-3oqJZnGYo6GlsKlZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3oqJZnGYo6GlsKlZ .error-icon{fill:#552222;}#mermaid-svg-3oqJZnGYo6GlsKlZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3oqJZnGYo6GlsKlZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .marker.cross{stroke:#333333;}#mermaid-svg-3oqJZnGYo6GlsKlZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3oqJZnGYo6GlsKlZ p{margin:0;}#mermaid-svg-3oqJZnGYo6GlsKlZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .cluster-label text{fill:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .cluster-label span{color:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .cluster-label span p{background-color:transparent;}#mermaid-svg-3oqJZnGYo6GlsKlZ .label text,#mermaid-svg-3oqJZnGYo6GlsKlZ span{fill:#333;color:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .node rect,#mermaid-svg-3oqJZnGYo6GlsKlZ .node circle,#mermaid-svg-3oqJZnGYo6GlsKlZ .node ellipse,#mermaid-svg-3oqJZnGYo6GlsKlZ .node polygon,#mermaid-svg-3oqJZnGYo6GlsKlZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .rough-node .label text,#mermaid-svg-3oqJZnGYo6GlsKlZ .node .label text,#mermaid-svg-3oqJZnGYo6GlsKlZ .image-shape .label,#mermaid-svg-3oqJZnGYo6GlsKlZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-3oqJZnGYo6GlsKlZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .rough-node .label,#mermaid-svg-3oqJZnGYo6GlsKlZ .node .label,#mermaid-svg-3oqJZnGYo6GlsKlZ .image-shape .label,#mermaid-svg-3oqJZnGYo6GlsKlZ .icon-shape .label{text-align:center;}#mermaid-svg-3oqJZnGYo6GlsKlZ .node.clickable{cursor:pointer;}#mermaid-svg-3oqJZnGYo6GlsKlZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .arrowheadPath{fill:#333333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3oqJZnGYo6GlsKlZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3oqJZnGYo6GlsKlZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3oqJZnGYo6GlsKlZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3oqJZnGYo6GlsKlZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .cluster text{fill:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ .cluster span{color:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3oqJZnGYo6GlsKlZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3oqJZnGYo6GlsKlZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-3oqJZnGYo6GlsKlZ .icon-shape,#mermaid-svg-3oqJZnGYo6GlsKlZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3oqJZnGYo6GlsKlZ .icon-shape p,#mermaid-svg-3oqJZnGYo6GlsKlZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3oqJZnGYo6GlsKlZ .icon-shape .label rect,#mermaid-svg-3oqJZnGYo6GlsKlZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3oqJZnGYo6GlsKlZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3oqJZnGYo6GlsKlZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3oqJZnGYo6GlsKlZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 401/502
HTTP 请求
网关 / BFF
业务服务
JSON: code message data traceId
前端按 code 处理
不要把所有失败都塞进 HTTP 500 ,也不要让前端只靠 HTTP 状态码区分「密码错误」和「库存不足」------后者应靠业务 code。
2. 设计原则
| 原则 | 说明 |
|---|---|
| 唯一性 | 一个 code 只表示一种错误,禁止复用 |
| 机器可读 | 程序只认 code / name 常量,不认自然语言 |
| 人类可读 | message 给展示与 i18n,不参与核心分支逻辑 |
| 不泄露内部 | 禁止向前端返回堆栈、SQL、内网 IP、未脱敏路径 |
| 可追踪 | 响应与日志带同一 traceId (及可选 spanId) |
| 可扩展 | 分段或命名空间,新模块新区间,避免撞号 |
| 中心化 | 定义在统一 YAML/Proto/仓库,禁止各服务私造「通用码」 |
进阶字段(可选):
| 字段 | 用途 |
|---|---|
type |
BUSINESS / SYSTEM / INFRA / AUTH / THIRD_PARTY |
retryable |
前端/客户端是否应退避重试 |
suggest_action |
如跳转登录、展示表单错误 |
3. 统一响应结构
推荐 JSON 信封(字段名可按团队微调,但语义保持一致):
json
{
"code": 20002,
"message": "密码错误",
"data": null,
"traceId": "a1b2c3d4e5f6"
}
path(请求路径)可选 :调试环境可返回;生产环境不少团队只在日志中记录 path,响应体不暴露,避免轻微泄露内部路由结构。
| 字段 | 说明 |
|---|---|
code |
整数或字符串常量;成功见下「成功约定」 |
message |
默认提示文案;可配合 i18n 键 |
data |
成功时业务数据;失败时常为 null 或结构化错误详情 |
traceId |
全链路 ID,用户报障与后端排障必备 |
path |
可选;生产宜仅写日志 |
成功约定 :本文示例以 code: 0 表示成功(国内前后端协作极常见)。Google Cloud API、AWS 等更常采用 HTTP 200 + 业务体直接返回 (无额外 code)或字符串 OK。选定一种后全公司统一即可,避免同一产品两套信封。
成功示例:
json
{
"code": 0,
"message": "ok",
"data": { "userId": "u_123" },
"traceId": "a1b2c3d4e5f6"
}
4. 错误码如何编号
4.1 分段数字(国内常见)
text
0 成功
1xxxx 通用(参数、鉴权、系统)
1000 参数校验失败
1001 未登录 / Token 过期 → 常配合 HTTP 401
1002 无权限 → 常配合 HTTP 403
1003 资源不存在 → 常配合 HTTP 404
1004 请求频率超限 → 常配合 HTTP 429
1999 系统内部错误 → 常配合 HTTP 500
2xxxx 用户模块
20001 用户不存在
20002 密码错误
3xxxx 订单模块
30001 库存不足
30002 订单已取消
小团队可简化为:0 + 按模块预留区间(如用户 10001--10999、订单 20001--20999)。
4.2 结构化长码
格式示意:[级别][模块][序号],例如 2011002 → 业务(2) + 用户(01) + 密码错误(002)。
4.3 字符串码(可读性优先)
如 B-USER-PASSWORD_WRONG、ORDER_STOCK_NOT_ENOUGH。适合 OpenAPI 文档与跨语言常量名对齐;注意大小写与命名规范统一。
5. 后端:全局异常与分层
各语言框架实现不同,但职责分层一致:
| 异常类型 | 典型映射 | 对外注意 |
|---|---|---|
| 参数校验失败 | code=1000,HTTP 400 或 200 |
可带字段级 data.errors |
| 业务异常(BizException) | 携带注册过的 code + message |
多为 HTTP 200 或 400 |
| 未捕获异常 | 记完整日志,对外 1999 或通用系统码 |
绝不返回堆栈 |
| 网关鉴权/限流 | 401 / 403 / 429 | 由网关或统一过滤器返回标准 JSON |
| 下游超时/不可用 | 502 / 503 | 与业务「库存不足」区分开 |
伪代码(语言无关):
text
try:
return success(data)
catch ValidationError as e:
return envelope(code=INVALID_PARAM, http=400)
catch BizError as e:
return envelope(code=e.code, message=e.message, http=200)
catch Exception as e:
log.error(traceId, e)
return envelope(code=INTERNAL_ERROR, http=500)
禁止 在业务代码里散落 magic number;应引用中心化定义的 ErrorCode 常量。
6. 前端:拦截器与分支策略
原则:HTTP 层 与业务层分开处理。
javascript
// 示意:axios 响应拦截
function onResponse(response) {
const body = response.data;
if (body.code === 0) {
return body.data;
}
switch (body.code) {
case 1001:
redirectToLogin();
break;
case 20002:
showToast('密码错误'); // 或 i18n(body.message)
break;
default:
showToast(body.message || '操作失败');
}
if (body.retryable) {
// 可选:指数退避重试,注意幂等与上限
scheduleRetry(requestConfig, body);
}
return Promise.reject(body);
}
function onHttpError(error) {
// 网络断开、502、504 等 --- 非业务 code
showToast('网络异常,请稍后重试');
}
| 规则 | 说明 |
|---|---|
用 code 做 switch |
不要 if (message.includes('密码')) |
| 非 2xx 进 HTTP 错误处理 | 与 body 里 code 两条线 |
上报带 traceId |
用户反馈时复制 traceId 给支持/后端 |
7. 两种流派与生产折中
| 流派 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| 全 200 + body code | 成功/失败 HTTP 均为 200 | 前端统一读 body;国内联调习惯 | 丢失 HTTP 语义;网关难按状态过滤 |
| HTTP + body code | 参数错 400、未登录 401;业务细码在 body | REST 清晰;CDN/网关友好 | 部分旧客户端对非 200 处理麻烦 |
生产常见折中:
- 网关 / 鉴权 / 限流 / 路由不存在 → 正确 HTTP 状态码
- 可预期的业务失败(余额不足、重复提交)→ HTTP 200 + code≠0
- 文档中写清每个接口的 HTTP 与 code 组合,避免前后端各自猜测
8. 微服务:挑战与传递规范
8.1 特殊挑战
| 挑战 | 后果 |
|---|---|
| 各服务自定规则 | BFF/前端大量映射 if-else |
| 中间层「吞」下游错误 | 前端只见「系统异常」,无法排障 |
| 责任不清 | 工单在网关、本服务、下游之间流转混乱 |
| 缺统一 trace | 多服务日志无法拼成一次用户请求 |
| 熔断与业务混淆 | 用户不知应重试还是改参数 |
8.2 传递原则
谁出错,谁定义;中间层只包装,不重写语义。
跨服务错误体建议保留:
json
{
"code": 30001,
"message": "库存不足",
"service": "inventory-service",
"original_code": 30001,
"original_service": "inventory-service",
"trace_id": "abc123",
"span_id": "def456"
}
❌ 反例:下游库存不足,上游统一成 { "code": 500, "message": "系统异常" }。
8.3 gRPC / Thrift 与 HTTP 归一化
内部微服务大量采用 gRPC (或 Thrift 等 RPC),错误模型与 HTTP JSON 不同,但业务语义应同源:
| 协议 | 常见载体 | 建议 |
|---|---|---|
| gRPC | status(如 INVALID_ARGUMENT)+ message + details |
在 details / error_details 中放 业务 code、retryable、trace_id (可用 google.rpc.Status + 自定义 Proto) |
| Thrift | 框架异常 + 应用错误码字段 | 与中心化 YAML/Proto 生成的常量对齐 |
| 对外 REST | JSON 信封 | 由 BFF / API 网关 将 RPC 错误 归一化 为本文 §3 结构,勿让前端直接解析 gRPC status |
原则不变:RPC 层保留 original_code / original_service;中间服务不吞下游业务语义。
8.4 HTTP 状态码由谁决定
| 场景 | HTTP | 说明 |
|---|---|---|
| 网关鉴权失败 | 401 | 网关直接返回 |
| 路由不存在 | 404 | 网关 / API 网关 |
| 限流 | 429 | 网关 |
| 业务失败 | 200 | body.code ≠ 0 |
| 下游不可用 | 502 / 503 | 网关或 BFF |
不要让每个微服务各自发明一套「HTTP 策略」;内部走 RPC 时由 status + details 表达,对外由 BFF/网关归一化。
8.5 错误分类:业务、系统、基础设施
| 类型 | type(示意) |
可重试 | 用户可见 | 示例 |
|---|---|---|---|---|
| 业务 | BUSINESS |
否 | 是 | 库存不足、格式错误 |
| 系统 | SYSTEM |
有时 | 通常否 | DB 连接失败 |
| 基础设施 | INFRA |
是 | 可选提示 | 熔断、降级、下游超时 |
熔断/降级归类为 INFRA / SYSTEM,而非业务错误。宜独立码段,例如:
text
9001 熔断(Circuit Breaker Open)
9002 降级(Fallback)
9003 下游超时
8.6 BFF / 网关归一化
- 统一 JSON 字段名与
code体系 - 将内部服务名、技术细节转为用户可读
message - 不要 把
inventory-service panic原文返回给前端
9. 中心化错误码治理
错误码是 API Schema 的一部分,不是某个仓库里的私有魔法数字。
#mermaid-svg-NwLLvxxLj82sO6L2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NwLLvxxLj82sO6L2 .error-icon{fill:#552222;}#mermaid-svg-NwLLvxxLj82sO6L2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NwLLvxxLj82sO6L2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NwLLvxxLj82sO6L2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NwLLvxxLj82sO6L2 .marker.cross{stroke:#333333;}#mermaid-svg-NwLLvxxLj82sO6L2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NwLLvxxLj82sO6L2 p{margin:0;}#mermaid-svg-NwLLvxxLj82sO6L2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 .cluster-label text{fill:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 .cluster-label span{color:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 .cluster-label span p{background-color:transparent;}#mermaid-svg-NwLLvxxLj82sO6L2 .label text,#mermaid-svg-NwLLvxxLj82sO6L2 span{fill:#333;color:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 .node rect,#mermaid-svg-NwLLvxxLj82sO6L2 .node circle,#mermaid-svg-NwLLvxxLj82sO6L2 .node ellipse,#mermaid-svg-NwLLvxxLj82sO6L2 .node polygon,#mermaid-svg-NwLLvxxLj82sO6L2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NwLLvxxLj82sO6L2 .rough-node .label text,#mermaid-svg-NwLLvxxLj82sO6L2 .node .label text,#mermaid-svg-NwLLvxxLj82sO6L2 .image-shape .label,#mermaid-svg-NwLLvxxLj82sO6L2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-NwLLvxxLj82sO6L2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NwLLvxxLj82sO6L2 .rough-node .label,#mermaid-svg-NwLLvxxLj82sO6L2 .node .label,#mermaid-svg-NwLLvxxLj82sO6L2 .image-shape .label,#mermaid-svg-NwLLvxxLj82sO6L2 .icon-shape .label{text-align:center;}#mermaid-svg-NwLLvxxLj82sO6L2 .node.clickable{cursor:pointer;}#mermaid-svg-NwLLvxxLj82sO6L2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NwLLvxxLj82sO6L2 .arrowheadPath{fill:#333333;}#mermaid-svg-NwLLvxxLj82sO6L2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NwLLvxxLj82sO6L2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NwLLvxxLj82sO6L2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NwLLvxxLj82sO6L2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NwLLvxxLj82sO6L2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NwLLvxxLj82sO6L2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NwLLvxxLj82sO6L2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NwLLvxxLj82sO6L2 .cluster text{fill:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 .cluster span{color:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NwLLvxxLj82sO6L2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NwLLvxxLj82sO6L2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-NwLLvxxLj82sO6L2 .icon-shape,#mermaid-svg-NwLLvxxLj82sO6L2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NwLLvxxLj82sO6L2 .icon-shape p,#mermaid-svg-NwLLvxxLj82sO6L2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NwLLvxxLj82sO6L2 .icon-shape .label rect,#mermaid-svg-NwLLvxxLj82sO6L2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NwLLvxxLj82sO6L2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NwLLvxxLj82sO6L2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NwLLvxxLj82sO6L2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} errors.yaml / Proto 定义
CI 校验唯一性区间
代码生成 Java Go TS
OpenAPI 错误示例
error-meta 告警映射
各微服务引用常量
9.1 元数据示例(YAML)
yaml
version: "1.0"
domains:
common:
range: 1000-1999
codes:
- code: 1000
name: INVALID_PARAM
http_status: 400
type: BUSINESS
retryable: false
summary_zh: 参数校验失败
- code: 1001
name: UNAUTHORIZED
http_status: 401
type: AUTH
retryable: false
summary_zh: 未登录或 Token 过期
order:
range: 30000-30999
codes:
- code: 30001
name: STOCK_NOT_ENOUGH
http_status: 200
type: BUSINESS
retryable: false
summary_zh: 库存不足
9.2 治理方式选型
| 规模 | 方式 |
|---|---|
| 小团队 | Git + YAML + MR 评审 + CI 生成 |
| 中大型 | 管理后台 + DB,定期 export 到 Git |
| 约束 | 禁止手写 magic number;CI 扫描硬编码 |
| SDK 使用 | 各服务只依赖 生成的常量 / SDK ;禁止 用反射、运行时解析 message 或动态字符串做分支------error-meta 变更会导致逻辑静默失效 |
9.3 版本
- 错误码规范带
version;服务声明依赖的 spec 版本 - 新增 code:向后兼容
- 修改语义 / 删除:major version bump,通知客户端
10. 与 OpenAPI、监控联动
10.1 OpenAPI
在接口 responses 中声明错误示例,而不仅是 200:
yaml
responses:
'200':
description: 业务成功或业务失败(见 code)
content:
application/json:
schema:
$ref: '#/components/schemas/ApiEnvelope'
examples:
success:
value: { code: 0, message: ok, data: {} }
stock_error:
value: { code: 30001, message: 库存不足, data: null }
每个接口文档应列出可能出现的业务错误码及处理建议(类似云厂商 OpenAPI 错误码表)。
10.2 监控与告警
导出 error-meta.json 供 Prometheus / Grafana 使用:
json
{
"30001": { "type": "BUSINESS", "severity": "INFO", "alert": false },
"5000": { "type": "SYSTEM", "severity": "CRITICAL", "alert": true }
}
- 指标:
api_error_total{code="30001",service="order"} - 业务错误 (库存不足)→ 统计转化率,不半夜 PagerDuty
- 系统 / INFRA 错误 → 按阈值告警
- SLO 视角 :业务
code通常 不应计入 SLI (如可用性、成功率);系统错误码才是 SLO 与**错误预算(Error Budget)**的主要依据------避免「库存不足」吃掉可用性指标
日志与 APM:每条错误日志带 trace_id、error_code、service;Span 标记 ERROR + code。
11. 业界方案速览
没有类似 MySQL 的「统一错误码标准产品」;大厂多 自研 Registry + Schema + CI 生成。
| 思路 | 代表 | 特点 |
|---|---|---|
| Schema + 生成器 | 自建 YAML + CI;Rust biz-error;Go error-framework |
无运行时依赖,最常用 |
| 管理平台 | err0、ErrorHub | Web UI + i18n + SDK 拉取;需考虑高可用 |
| 框架内置 | 斗鱼 Jupiter(Go)、go-zero 示例 | 错误码分段 + 文档/监控标签 |
| 大厂闭源参考 | 字节 Protobuf error_code、腾讯多协议 Registry | gRPC/HTTP/Thrift 映射一致 |
| 团队规模 | 建议 |
|---|---|
| ≤20 服务 | Git YAML + CI 生成(Java/Go/TS) |
| 多语言 + 要 UI | 评估 err0 / ErrorHub |
| 已有 gRPC 体系 | Proto 定义 error details + 代码生成 |
12. 反模式
| 反模式 | 后果 |
|---|---|
前端用 message 字符串匹配 |
i18n/改文案即逻辑 bug |
同一 code 多种含义 |
监控与客户端行为混乱 |
| 下游错误包装成 500 | 排障地狱 |
| 业务错误触发系统告警 | 库存不足半夜叫醒 oncall |
| 各服务私造 1001「未登录」 | 网关映射爆炸 |
| 响应带堆栈/SQL | 安全风险 |
| 反射/动态字符串判断 code | error-meta 变更后逻辑静默错误 |
13. 落地阶段建议
| 阶段 | 周期(示意) | 内容 |
|---|---|---|
| 一 | 1--2 周 | errors.yaml + CI 校验 + 多语言常量生成;新接口强制用常量 |
| 二 | 1--2 月 | OpenAPI 注入错误示例;日志/监控按 BUSINESS/SYSTEM 拆分 |
| 三 | 持续 | 管理平台、i18n 文案流水线、告警模板、前端 SDK |
14. 名词速查卡
text
┌────────────────────────────────────────────────────────────┐
│ HTTP 状态码 = 协议/基础设施;body.code = 业务语义 │
│ 成功 code=0;逻辑分支看 code,不看 message │
│ traceId 贯穿响应、日志、APM │
│ 微服务:不吞 original_code;BFF 归一化对外文案 │
│ 错误码 = Schema → CI 生成 → 文档 + 监控 │
└────────────────────────────────────────────────────────────┘
延伸阅读
| 资源 | 链接 |
|---|---|
| HTTP 状态码 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Status |
| OpenAPI 规范 | https://spec.openapis.org/ |
| OpenTelemetry 追踪 | https://opentelemetry.io/docs/ |
| Google API 错误模型(参考) | https://cloud.google.com/apis/design/errors |
| gRPC 状态码 | https://grpc.io/docs/guides/status-codes/ |
一句话 :好的错误码体系 = HTTP 管传输、code 管业务、信封统一、trace 可追溯 ;在微服务里还要 不吞语义、中心化治理,让文档、代码与监控读同一套定义。