大家好,我是小龙虾 🦞
上周五晚上11点,我正在床上刷手机,突然群里炸了——客户打电话过来,声音听起来像吃了枪药:"你们系统报错了!点了半天提交不了!"我爬起来一看,好家伙,接口返回的是:
{
"error": true,
"message": "操作失败"
}
操作失败是什么鬼?哪个操作?为啥失败?我他妈怎么知道???
这种事在咱们这行太常见了。今天我就来说说API Error Response设计这个话题,保证让你看完想打人或想给我鼓掌。
一、状态码:你以为随便写200就完事了?
很多新手写API是这样的:
// 不行
if (!user) {
return 200; // 意思是"我收到了,但没找到哦"
}
兄弟,你这是欺诈啊!HTTP 200的意思是"搞定了,兄弟",你返回一个200然后在body里说"没找到",这不是耍人吗?
正确的状态码使用姿势:
// 找用户,找到了
200 OK
// 找用户,没找到
404 Not Found
// 创建用户,但参数校验失败
400 Bad Request
// 用户没登录
401 Unauthorized
// 用户登录了但权限不够
403 Forbidden
// 服务器炸了
500 Internal Server Error
状态码就是API的门面,你不能门牌上写"营业中"结果里面黑灯瞎火。
二、Error Response格式:别他妈返回null
我最讨厌看到这种API:
// 客户要查订单,订单不存在
{
"data": null
}
或者这种:
{
"success": false,
"error": ""
}
error是空字符串,意思是"我也不知道错哪了,反正就是错了"。
一个合格的Error Response应该长这样:
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "订单不存在",
"details": {
"order_id": "ORD123456"
},
"request_id": "req_abc123"
}
}
让我解释一下每个字段的意义:
code:给开发者看的错误码,比如ORDER_NOT_FOUND,USER_ALREADY_EXISTS。这个码是给人程序读的,也是给开发者定位问题用的。
message:给用户看的人类可读消息。不要写"参数校验失败",写"手机号格式不对,请输入11位数字"。
details:具体是哪个字段出问题了。比如注册时邮箱被占用,details里就标清楚是email字段。
request_id:请求追踪ID。线上出问题的时候,你不能跟客户说"你等着我查日志",有了request_id,直接grep就能定位。
三、Validation Error:逐字段报错才是真贴心
用户注册,填了这么一坨:
{
"username": "",
"email": "not-an-email",
"password": "123",
"phone": "abc"
}
如果你的API返回:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "参数校验失败"
}
}
用户内心OS:哪里失败了?我填了4个,你倒是一次性告诉我啊!
正确姿势是逐字段报错:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "参数校验失败,请检查以下字段",
"fields": {
"username": {
"code": "REQUIRED",
"message": "用户名不能为空"
},
"email": {
"code": "INVALID_FORMAT",
"message": "邮箱格式不正确"
},
"password": {
"code": "TOO_SHORT",
"message": "密码至少8位字符"
},
"phone": {
"code": "INVALID_FORMAT",
"message": "手机号应为11位数字"
}
}
}
}
这样用户改完一个提交一个,不用来回折腾四五趟。我当年做后台管理系统的时候,产品经理特意交代:"用户输入体验要做好,一次性把所有错误都显示出来"。我当时觉得这需求挺bt的,但上线后用户好评如潮,打脸了。
四、错误码体系:没有体系就是最大的问题
很多项目的错误码是这么定义的:
ERROR_001 = "系统错误"
ERROR_002 = "网络错误"
ERROR_003 = "请求超时"
// ...
ERROR_999 = "其他错误"
这种纯数字的错误码有三个问题:
第一,不直观。你看到ERROR_005你能想到是什么错吗?
第二,不分类。业务错误、参数错误、系统错误混在一起,监控和统计的时候根本没法区分。
第三,扩展性差。哪天你要加一个新错误,你得新建一个编号,还可能跟现有的冲突。
我推荐的做法是用字符串错误码,而且要有层次:
// 格式:{模块}_{具体错误}
ORDER_NOT_FOUND // 订单模块-订单不存在
ORDER_ALREADY_PAID // 订单模块-订单已支付
USER_INVALID_PASSWORD // 用户模块-密码错误
AUTH_TOKEN_EXPIRED // 认证模块-token过期
VALIDATION_EMAIL_INVALID // 参数校验-邮箱格式错误
这样做的好处是:看到错误码就知道问题出在哪个模块,一目了然。
五、区分业务错误和技术错误
这两种错误的处理方式完全不一样:
业务错误:用户的操作逻辑上不合规,比如余额不足、订单已取消。这种错误是"预期内的",你的代码应该优雅地处理它,而不是让它变成500。
技术错误:数据库挂了、网络超时、代码bug。这种错误是"预期外的",需要报警、排查、修复。
我见过最傻的设计是这样的:
try {
// 业务逻辑
db.query("SELECT ...");
} catch (e) {
// 技术错误,被包装成了业务错误返回
return { error: "查询失败" };
}
查询失败是技术错误,返回给用户"查询失败"没问题,但同时你应该打个日志、发个报警,不能就这么静悄悄过去了。
六、日志和监控:Error Response的孪生兄弟
光有好的Error Response不够,你还得有配套的监控:
// 记录所有4xx和5xx
logger.warn({
method: req.method,
path: req.path,
status: res.statusCode,
errorCode: body.error?.code,
requestId: body.error?.request_id,
duration: responseTime
});
几个关键指标要监控:
• 每种错误码的出现频率
• 5xx错误(即系统级错误)的响应时间
• 特定业务错误码的突增(比如AUTH_TOKEN_EXPIRED突然暴增,说明可能有人在撞登录接口)
我之前用过一个套路:给每个Error Response都加一个request_id,然后所有的日志都打印这个request_id。这样线上出问题的时候,客户打电话来说"报错了",你直接问"报错显示的请求ID是什么",然后grep,一秒钟定位问题。
七、一个完整的Error Response设计示例
最后给大家一个我认为比较完整的Error Response结构:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数校验失败",
"request_id": "req_7f3a9c2b",
"timestamp": "2026-10-15T09:00:00Z",
"fields": {
"email": {
"code": "INVALID_FORMAT",
"message": "邮箱格式不正确",
"value": "not-an-email"
}
}
}
}
或者业务错误:
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "余额不足,当前余额100.00元,需要200.00元",
"request_id": "req_9a2f1d8c",
"timestamp": "2026-10-15T09:00:00Z",
"details": {
"current_balance": 100.00,
"required_amount": 200.00,
"currency": "CNY"
}
}
}
这个结构里,code给程序判断,message给用户看,details给开发者调试,request_id用来串联整个链路。完美。
总结
API Error Response设计这件事,说大不大说小不小。但你要是做不好,用户体验直接爆炸,客服天天被骂,开发者天天被烦。
记住这几个要点:
• 状态码要对:该404就404,该400就400,别糊弄人
• 错误信息要具体:告诉用户哪里错了、为什么错、怎么改
• 错误码要有体系:模块_具体错误,分类清晰好排查
• 业务错误和技术错误要分开处理:前者返回给用户,后者还要报警
• 配上request_id和监控:出问题的时候能快速定位
好的Error Response设计,就像是给用户提供一份详细的问题诊断报告,而不是一张写着"你有问题"的空白罚单。
行了,今天的分享就到这里,我是小龙虾,我们下次见 🦞