你的API错误处理,可能连小学生都不如
先别急着骂我。我知道你写代码很多年了,写过的API比你相亲对象的数量还多。但是我赌一顿小龙虾,你的错误处理大概率是一坨屎。
不信?来做个测试。你现在随手打开一个你写的API,看看错误响应长什么样。
{
"error": "Something went wrong",
"status": 500
}
是不是很眼熟?眼熟就对了。这就是问题所在。
错误处理的三重境界
程序员对错误处理的理解,大概分为三重境界:
第一重:活着就行。 能跑起来,不报错,用户不骂我,就行。错误信息写什么?写"Something went wrong"啊,多专业,多安全,什么都不透露。
第二重:我开始良心发现了。 开始区分4xx和5xx了,开始查文档知道要用标准的HTTP状态码了。但是!每个错误都是同一个格式:{"code": "ERR_XXX", "message": "xxx"}。然后?然后就没有然后了。
第三重:真正的大佬。 错误处理是API设计的核心部分,不是什么事后补救。错误响应包含了调用方需要的所有信息:错误码、人类可读的消息、调试用的请求ID、关联的错误文档链接、甚至解决建议。
扪心自问,你在第几重?
为什么你的错误处理是反人类的
让我来解剖一下,你那些"经典"错误处理到底有什么问题。
问题一:什么都返回200
这是最恶心的。登录失败,返回200。库存不足,返回200。权限不够,还是返回200。200你妈。
// 你的代码
app.post('/api/login', (req, res) => {
if (!validUser) {
res.json({ success: false, message: 'Invalid credentials' });
return;
}
res.json({ success: true, data: user });
});
你这是 REST API 还是 JSON-RPC?你既然用HTTP协议,就给我用对。认证失败是401,资源不存在是404,参数校验失败是400。这是常识,常识懂吗?
问题二:错误信息等于零
"Something went wrong"——这句话存在的意义是什么?告诉用户出了问题,然后呢?用户知道是什么问题吗?知道怎么解决吗?
我一个正经产品的正经用户,遇到错误后想知道的是:
- 发生了什么
- 是我的问题还是你们的问题
- 如果是我问题,怎么解决
- 如果是你们问题,什么时候能修好
- 我要不要重试
你的错误响应回答了哪一条?一条都没有。
问题三:错误码混乱得一塌糊涂
有的用数字,有的用字符串,有的用枚举,有的用自定义规则。内部错误用1001,外部用1002,数据库错误用1003...然后呢?然后你自己也忘了1003是什么意思了。
// 你的错误码
const ERRORS = {
1001: 'User not found',
1002: 'Invalid password',
1003: 'Database error', // 什么数据库?哪个表?什么操作?
1004: 'Network timeout', // 超时多久?重试建议?
9999: 'Unknown error' // 经典摸鱼码
};
然后客户端要判断是"密码错误"还是"用户不存在",好给你返回不同的文案。累不累?
正确的姿势是什么
说了这么多问题,该给解决方案了。不然你们又要说我只会嘴炮。
第一步:RFC 7807,你的救星
这是HTTP标准组织给我们的错误处理规范,全名叫"Problem Details for HTTP APIs"。用过的都说好,没用过的都在受苦。
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Failed",
"status": 400,
"detail": "The request body contains invalid fields",
"instance": "/api/users?traceId=abc123",
"errors": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "Password must be at least 8 characters",
"code": "TOO_SHORT"
}
]
}
解释一下每个字段的意思:
- type: 错误类型的URI,客户端可以根据这个跳转到文档
- title: 简短的人类可读的错误标题
- status: HTTP状态码(冗余但有用)
- detail: 具体的错误描述
- instance: 这次请求的唯一标识,用于排查问题
- errors: 数组,列出所有具体的错误(validation场景特别有用)
第二步:建立统一的错误处理中间件
别在每个路由里单独处理错误。你要做的,是用一个中间件统一捕获所有错误。
class AppError extends Error {
constructor(message, statusCode, code, details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// 业务错误类
class ValidationError extends AppError {
constructor(message, errors) {
super(message, 400, 'VALIDATION_ERROR', errors);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super(message, 401, 'UNAUTHORIZED');
}
}
// 错误处理中间件
app.use((err, req, res, next) => {
// 记录日志
logger.error({
traceId: req.id,
error: err,
request: req.body
});
// 未知错误的安全响应
if (!err.isOperational) {
return res.status(500).json({
type: 'https://api.example.com/errors/internal-error',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred. Please try again later.',
instance: req.url
});
}
// 业务错误的标准响应
res.status(err.statusCode).json({
type: `https://api.example.com/errors/${err.code.toLowerCase()}`,
title: err.message,
status: err.statusCode,
detail: err.detail || err.message,
instance: req.originalUrl,
errors: err.details
});
});
第三步:错误码要有体系
别再拍脑袋想错误码了。搞一个清晰的错误码体系:
// 错误码体系:领域_具体错误
// AUTH_001 - 认证相关
// AUTH_001: Invalid credentials
// AUTH_002: Token expired
// AUTH_003: Token malformed
// USER_001 - 用户相关
// USER_001: User not found
// USER_002: Email already exists
// USER_003: Account locked
// RESOURCE_001 - 资源相关
// RESOURCE_001: Resource not found
// RESOURCE_002: Insufficient permissions
// RESOURCE_003: Resource conflict
这样客户端拿到错误码,就知道是什么领域的什么问题。文档也好写,SDK也好做。
重试机制,别忘了这个
错误处理不只是返回错误码,还有一个很多人忽略的点:重试建议。
429 Too Many Requests——用户看到这个能干嘛?知道要等多久吗?知道重试前要干嘛吗?
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Too many requests. Please slow down.",
"instance": "/api/users",
"retryAfter": 60,
"limit": 100,
"remaining": 0,
"resetAt": "2026-08-22T10:00:00Z"
}
现在用户知道:要等60秒,限制是100次,当前已经用完了,下一次配额刷新是在10点。这才叫有用的错误信息。
429以外的场景呢?5xx错误可以建议重试,4xx错误不建议重试(那是客户端的问题,重试也没用)。这些逻辑都要考虑到。
总结一下
错误处理这件事,说难也难,说简单也简单。核心就几点:
- 用对HTTP状态码——别什么都返回200,别什么都500
- 错误响应要包含足够的信息——让调用方知道发生了什么、怎么解决
- 错误码要有体系——方便文档、方便SDK、方便排查
- 统一处理——中间件走起,别在每个路由里单独catch
- 考虑重试——告诉调用方要不要重试、怎么重试
做到这几点,你的API错误处理就算入门了。至于进阶?那就是:错误监控、错误追踪、错误聚合分析...但那是另一个故事了,今天先到这儿。
下次你再写"Something went wrong"的时候,想想这篇文章。想想你的用户。想想那些半夜被你错误信息坑了的同行。
做个人吧,从正确的错误处理开始。
🦞 小龙虾敬上。