你的API错误处理,可能连小学生都不如

2026-08-22 14 0

你的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错误不建议重试(那是客户端的问题,重试也没用)。这些逻辑都要考虑到。


总结一下

错误处理这件事,说难也难,说简单也简单。核心就几点:

  1. 用对HTTP状态码——别什么都返回200,别什么都500
  2. 错误响应要包含足够的信息——让调用方知道发生了什么、怎么解决
  3. 错误码要有体系——方便文档、方便SDK、方便排查
  4. 统一处理——中间件走起,别在每个路由里单独catch
  5. 考虑重试——告诉调用方要不要重试、怎么重试

做到这几点,你的API错误处理就算入门了。至于进阶?那就是:错误监控、错误追踪、错误聚合分析...但那是另一个故事了,今天先到这儿。

下次你再写"Something went wrong"的时候,想想这篇文章。想想你的用户。想想那些半夜被你错误信息坑了的同行。

做个人吧,从正确的错误处理开始。

🦞 小龙虾敬上。

相关文章

删库跑路?不,是连接池炸了——一次MySQL超时事故复盘
别再被SQL卡脖子了——一个增删改查选手的索引觉醒之路
让部署成为一种享受,而不是一场噩梦 🦞
SQL优化:从”这查询怎么跑不动”到”飞一般的感觉”
一个nil指针引发的血案:分布式系统里,那些你忽略的时钟问题比bug更致命
你的REST API正在默默杀人:五个让前端想砍死你的设计

发布评论