如何处理接口错误

错误处理的好坏,直接影响用户体验和调试效率

很多开发者在写 API 的时候,把大部分精力放在「正常流程」上——参数校验通过、数据库查询成功、业务逻辑跑通、返回 200 OK。至于错误处理,往往是随手写一个 res.status(500).json({ error: 'Something went wrong' }),觉得够用就行。

这种态度在个人项目里或许没问题,但当你的 AI 产品开始面向海外用户,当第三方开发者通过 API 集成你的服务,当线上出现偶发故障需要快速定位时,粗糙的错误处理会变成一场噩梦。用户看到「Something went wrong」不知道该怎么办,开发者对着没有错误码的响应没法自动重试,运维团队拿着一堆 500 日志分不清是哪个环节出了问题。

好的错误处理不是一次性工作,而是一套完整的设计体系:从错误码的编码规则,到响应格式的统一规范,到 HTTP Status Code 的正确使用,到前端的用户友好提示,到后端的日志记录和监控告警。这些环节缺一不可。

本文会系统讲清楚六个方面:

  1. 错误码设计原则——怎么编码才有意义、可扩展
  2. 错误响应格式——统一结构应该包含哪些字段
  3. 常见错误类型——4xx 客户端错误、5xx 服务端错误、业务错误的区分
  4. 错误处理最佳实践——日志、监控、重试、降级的工程策略
  5. 前端错误处理——怎么把技术错误翻译成用户能理解的提示
  6. 完整案例——从错误码定义到前端展示的全链路实现

错误码设计原则

错误码是错误处理体系的基石。一个设计良好的错误码体系,能让开发者看到错误码就知道问题出在哪里、应该怎么处理,而不需要去翻阅文档。

设计错误码的三个核心原则

1. 标准化

错误码应该遵循某种标准或约定,而不是凭感觉随意编号。最常见的做法是基于 HTTP Status Code 做业务层扩展。HTTP Status Code 解决了「协议层」的错误分类(4xx 是客户端问题,5xx 是服务端问题),但不够精确——400 Bad Request 可能对应十几种不同的参数校验失败。你需要在 HTTP Status Code 之上加一层业务错误码,让调用方能准确定位问题。

2. 可扩展

错误码体系要能随业务增长而扩展,不能出现「错误码用完了」或者「新增一个模块就要重新编号」的情况。推荐的做法是在错误码中嵌入模块标识,比如用 10001 表示用户模块的第一个错误,20001 表示订单模块的第一个错误。这样每个模块有独立的编号空间,互不干扰。

3. 有意义

错误码本身应该能帮助定位问题,而不是一个纯随机数。AUTH_001E10086 更容易理解。但也不建议用纯字符串做错误码——字符串没有数值比较能力,在一些编程语言里处理起来也不如数字方便。比较好的折中方案是:数字错误码 + 字符串错误类型(error code + error type),两者一起返回。

错误码分段设计

错误码范围错误类型说明
10000-10999通用错误参数校验、认证授权、限流等公共错误
11000-11999用户模块注册、登录、个人信息相关错误
12000-12999AI 能力模块模型调用、Token 额度、生成任务相关错误
13000-13999订单与支付订阅、扣费、退款相关错误
14000-14999内容与资源文件上传、素材管理、内容审核相关错误
15000-15999第三方集成Webhook、OAuth、外部 API 调用相关错误

错误码设计对比

维度纯数字方案纯字符串方案数字 + 字符串组合(推荐)
示例10001USER_NOT_FOUND{ code: 11001, type: "USER_NOT_FOUND" }
可读性差,需要查表好,一目了然好,机器和人类都能理解
可扩展性好,按模块分段一般,命名需要规范约束好,两段独立扩展
国际化支持不直接支持天然支持错误类型可用于 i18n key
前端判断便利性用数字 switch用字符串比较两种都支持,灵活度高
文档维护成本高,数字需要映射表中,命名即文档中,两者互为补充

错误响应格式

当错误发生时,API 返回的响应体应该包含足够信息,让调用方既能理解发生了什么,也能拿到程序化处理的依据。一个好的错误响应格式需要解决三个问题:这是什么错误、发生了什么、我需要什么额外信息。

统一错误响应结构

业界有两种主流的格式参考:

RFC 7807 Problem Details:IETF 标准化的错误响应格式,定义了 type(错误类型 URI)、title(简短描述)、status(HTTP 状态码)、detail(详细说明)、instance(具体发生位置)五个字段。适合标准化要求高的场景。

自定义 JSON 格式:在实际项目中更常见,灵活度更高。推荐的最小字段集合:

{
  "error": {
    "code": 12001,
    "type": "INSUFFICIENT_TOKENS",
    "message": "Token 额度不足,无法完成本次生成",
    "requestId": "req_abc123xyz",
    "details": {
      "field": "token_balance",
      "current": 0,
      "required": 500
    }
  }
}

错误响应字段说明

字段类型是否必需说明
codenumber业务错误码,用于程序化判断
typestring错误类型标识,可作为 i18n key
messagestring人类可读的错误描述
requestIdstring推荐请求唯一 ID,用于日志追踪
detailsobject可选错误详情,如字段级别的校验错误
timestampstring可选错误发生时间(ISO 8601 格式)
pathstring可选请求路径
helpUrlstring可选帮助文档链接

两种格式对比

维度RFC 7807自定义 JSON(推荐)
标准化程度IETF 标准,跨语言通用项目自定义,灵活度高
字段结构{ type, title, status, detail, instance }{ code, type, message, requestId, details }
业务错误码不直接支持原生支持
请求追踪需要自定义扩展原生支持 requestId
字段级错误需要自定义扩展原生支持 details
适用场景开放 API、标准化要求高的平台SaaS 产品、AI 平台 API

常见错误类型

API 错误可以按来源分为三大类:客户端错误(4xx)、服务端错误(5xx)和业务逻辑错误。理解它们的区别,才能做出正确的处理策略。

HTTP 4xx 客户端错误

客户端错误意味着「请求有问题」,责任在调用方。

HTTP Status使用场景注意事项
400 Bad Request请求格式错误、JSON 解析失败不要把所有 4xx 都扔给 400
401 Unauthorized未认证或认证信息过期区分「未登录」和「Token 过期」
403 Forbidden已认证但无权限访问该资源与 401 区分:401 是「你是谁」,403 是「你能不能」
404 Not Found请求的资源不存在用于资源级别,不要用于业务逻辑判断
409 Conflict资源冲突(如重复创建)常用于唯一约束冲突
422 Unprocessable Entity请求格式正确但语义错误比 400 更精确,推荐用于参数校验失败
429 Too Many Requests请求频率超出限制必须返回 Retry-After 头部

一个常见反模式是把所有客户端错误都返回 400。这会让调用方无法通过 HTTP Status 快速区分「认证失败」「权限不足」「参数错误」等完全不同的问题。

HTTP 5xx 服务端错误

服务端错误意味着「服务器出了问题」,责任在服务提供方。

HTTP Status使用场景处理建议
500 Internal Server Error未预期的异常记录完整日志,对外只返回通用提示
502 Bad Gateway上游服务返回异常响应检查网关配置和上游服务健康状态
503 Service Unavailable服务暂时不可用(维护或过载)返回 Retry-After 头部,配合降级页面
504 Gateway Timeout上游服务超时检查上游服务性能和超时配置

5xx 错误的关键原则是:对外不暴露内部细节。返回给客户端的 message 只需要说「服务暂时不可用」,不要返回堆栈信息、数据库连接字符串、内部 IP 地址。这些细节只应该出现在内部日志里。

业务逻辑错误

业务错误是 HTTP Status Code 覆盖不了的——它们不属于协议层面的问题,而是业务规则的违反。比如「Token 额度不足」「AI 生成任务排队超时」「订阅计划不支持该功能」。业务错误通常搭配 HTTP 200 或 422 返回,通过响应体中的业务错误码来区分。

错误类型完整对照

错误分类HTTP Status业务错误码是否可重试前端处理策略
参数校验失败42210001-10099否,需要修改参数展示字段级别的错误提示
认证失败40110101-10199否,需要重新登录跳转登录页
权限不足40310201-10299否,需要升级权限展示权限不足提示
资源不存在40410301-10399否,资源确实不存在展示 404 页面
请求频率限制42910401-10499是,按 Retry-After 等待提示稍后再试
Token 额度不足200/42212001-12099否,需要充值引导到充值页面
AI 模型调用失败200/50212101-12199是,自动重试展示加载状态或降级提示
服务内部异常50099001-99999是,自动重试展示通用错误提示

错误处理最佳实践

错误处理不只是「返回一个错误响应」这么简单。一个成熟的错误处理体系,需要覆盖日志记录、监控告警、自动重试和降级策略四个层面。

日志记录

每一次错误都应该被记录,但记录的粒度和内容需要讲究:

  • 结构化日志:使用 JSON 格式记录日志,包含 timestamplevelrequestIderror.codeerror.typeerror.messagestack(仅内部日志)、userIdpathmethod 等字段。结构化日志可以被日志系统(如 ELK、Datadog)高效检索和分析。
  • 脱敏处理:日志中不要记录用户密码、API Key、支付信息等敏感数据。如果必须记录请求体用于调试,对敏感字段做掩码处理。
  • 分级记录:4xx 错误用 WARN 级别,5xx 错误用 ERROR 级别,安全相关错误(如暴力破解)用 ALERT 级别。不同级别对应不同的告警策略。

监控告警

日志是「事后查」,监控告警是「实时发现」:

  • 错误率监控:当某个 API 的错误率在短时间内超过阈值(如 5 分钟内 5xx 错误率超过 1%),触发告警。
  • 错误分类统计:按 error.type 维度统计错误分布,帮助快速定位是某个特定问题还是大面积故障。
  • P99 延迟关联:错误率上升常常伴随延迟上升,将两者关联展示可以更快定位根因。

自动重试策略

不是所有错误都应该重试。重试策略需要根据错误类型区分:

async function requestWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options)
 
      // 4xx 错误不重试——问题在请求本身
      if (response.status >= 400 && response.status < 500) {
        const error = await response.json()
        throw new ApiError(error)
      }
 
      // 5xx 错误可以重试——问题在服务端
      if (response.status >= 500 && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000 // 指数退避
        await new Promise(resolve => setTimeout(resolve, delay))
        continue
      }
 
      return response
    } catch (error) {
      // 网络错误可以重试
      if (attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000
        await new Promise(resolve => setTimeout(resolve, delay))
        continue
      }
      throw error
    }
  }
}

关键原则:

  • 4xx 错误不重试:参数错了就是错了,重试一万次也不会自动变对。唯一的例外是 401(Token 过期),可以在刷新 Token 后重试。
  • 5xx 错误可以重试:服务端临时故障(503)或超时(504),重试有可能成功。
  • 指数退避:重试间隔逐次翻倍(1s → 2s → 4s),避免对服务端造成更大压力。
  • 幂等性检查:只有幂等操作(GET、PUT、DELETE)才应该自动重试。POST 请求的重试需要确认服务端支持幂等(通过 Idempotency-Key 头部)。

降级策略

当 AI 模型调用失败或超时时,可以考虑降级策略:

  • 模型降级:主力模型(如 GPT-4)不可用时,自动切换到备选模型(如 GPT-3.5)。对用户体验有一定影响,但比完全不可用好。
  • 缓存降级:返回缓存的最近一次成功结果,同时标注「这是缓存数据,可能不是最新」。
  • 异步降级:将同步请求转为异步任务——告诉用户「你的请求已收到,处理完成后会通知你」,而不是让用户一直等待。

前端错误处理

后端返回的错误信息是给开发者看的,前端还需要做一层「翻译」,把技术错误转化成用户能理解的提示。

错误提示的三个原则

1. 不要暴露技术细节

用户不需要知道「Database connection timeout at pool-3」。他们需要知道的是「服务暂时繁忙,请稍后再试」。技术细节可以出现在开发者控制台或日志系统里,但不应该出现在用户界面上。

2. 告诉用户可以做什么

好的错误提示不只是说「出错了」,还告诉用户下一步该怎么办:

错误场景差的提示好的提示
Token 额度不足「错误码 12001:INSUFFICIENT_TOKENS」「当前 Token 额度不足,充值后即可继续生成」
网络错误「fetch failed: ECONNREFUSED」「网络连接异常,请检查网络后重试」
文件上传失败「422 Unprocessable Entity」「文件格式不支持,请上传 PNG 或 JPG 格式的图片」
权限不足「403 Forbidden」「当前计划不支持此功能,升级专业版即可解锁」

3. 区分错误的严重程度

不同严重程度的错误,用不同的 UI 形式展示:

  • 轻量提示(Toast):参数校验失败、非关键操作的错误。自动消失,不打断用户操作。
  • 对话框(Dialog):需要用户做出决策的错误,如「会话已过期,请重新登录」。
  • 页面级别(Error Page):大面积服务不可用,如 503 降级页面。
  • 内联提示(Inline):表单字段级别的校验错误,直接在输入框下方展示。

前端错误处理实现示例

// 统一的 API 错误处理层
async function handleApiError(error: unknown) {
  if (error instanceof ApiError) {
    switch (error.type) {
      case 'UNAUTHORIZED':
      case 'TOKEN_EXPIRED':
        // 尝试刷新 Token,失败则跳转登录
        await tryRefreshToken()
        break
 
      case 'RATE_LIMITED':
        // 从 Retry-After 头部获取等待时间
        const retryAfter = error.headers?.get('Retry-After') ?? '60'
        showToast(`请求过于频繁,请 ${retryAfter} 秒后重试`)
        break
 
      case 'INSUFFICIENT_TOKENS':
        // 引导到充值页面
        showUpgradeDialog('Token 额度不足,充值后即可继续使用')
        break
 
      case 'FORBIDDEN':
        showUpgradeDialog('此功能需要专业版计划')
        break
 
      case 'VALIDATION_ERROR':
        // 字段级错误交给表单组件展示
        return error.details
        break
 
      default:
        // 未预期的业务错误
        showToast(error.message ?? '操作失败,请稍后再试')
    }
  } else if (error instanceof NetworkError) {
    showToast('网络连接异常,请检查网络设置')
  } else {
    // 未知错误,上报到错误监控系统
    reportError(error)
    showToast('服务暂时不可用,请稍后再试')
  }
}

全局错误边界

对于 React 应用,还需要设置 Error Boundary 来捕获渲染过程中的意外错误,防止整个页面白屏:

class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  state = { hasError: false, error: null }
 
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error }
  }
 
  componentDidCatch(error: Error, info: React.ErrorInfo) {
    // 上报到监控平台
    reportError(error, { componentStack: info.componentStack })
  }
 
  render() {
    if (this.state.hasError) {
      return <ErrorPage onRetry={() => this.setState({ hasError: false })} />
    }
    return this.props.children
  }
}

错误处理全流程

下面用一个 Mermaid 流程图展示完整的错误处理流程——从请求发出到最终响应,每一步的错误如何处理:

流程图画布 · 115%
Mermaid 流程图加载中...

实战案例

案例一:AI 生成接口的错误处理

假设你有一个 AI 文本生成接口 POST /api/v1/generate,用户提交 Prompt,服务端调用 AI 模型生成内容。这个接口可能遇到以下错误场景:

// 后端错误处理示例
async function handleGenerate(req: Request, res: Response) {
  const requestId = generateRequestId()
 
  try {
    // 1. 参数校验
    const { prompt, model } = req.body
    if (!prompt || prompt.length > 10000) {
      return res.status(422).json({
        error: {
          code: 10001,
          type: 'INVALID_PROMPT',
          message: 'Prompt 不能为空且长度不能超过 10000 字符',
          requestId,
        }
      })
    }
 
    // 2. 额度检查
    const balance = await getUserTokenBalance(req.userId)
    const required = estimateTokenCost(prompt)
    if (balance < required) {
      return res.status(422).json({
        error: {
          code: 12001,
          type: 'INSUFFICIENT_TOKENS',
          message: `Token 额度不足,当前 ${balance},需要 ${required}`,
          requestId,
          details: { current: balance, required }
        }
      })
    }
 
    // 3. 调用 AI 模型(含重试逻辑)
    const result = await callModelWithRetry(model, prompt, {
      maxRetries: 2,
      fallbackModel: 'gpt-3.5-turbo'
    })
 
    // 4. 扣减额度
    await deductTokens(req.userId, result.tokensUsed)
 
    return res.json({ data: { content: result.content, tokensUsed: result.tokensUsed } })
 
  } catch (error) {
    // 5. 未预期异常
    logger.error('Generate failed', { requestId, userId: req.userId, error })
    return res.status(500).json({
      error: {
        code: 99001,
        type: 'INTERNAL_ERROR',
        message: '服务暂时不可用,请稍后再试',
        requestId
      }
    })
  }
}

前端对应的错误处理:

async function generateContent(prompt: string) {
  try {
    const response = await fetch('/api/v1/generate', {
      method: 'POST',
      body: JSON.stringify({ prompt, model: 'gpt-4' })
    })
    const data = await response.json()
 
    if (!response.ok) {
      const { error } = data
      switch (error.type) {
        case 'INSUFFICIENT_TOKENS':
          showUpgradeDialog(`额度不足,当前 ${error.details.current},需要 ${error.details.required}`)
          break
        case 'INVALID_PROMPT':
          // 字段级错误,展示在输入框下方
          setFieldError('prompt', error.message)
          break
        case 'RATE_LIMITED':
          showToast('操作太频繁,请稍后再试')
          break
        default:
          showToast(error.message)
      }
      return
    }
 
    // 成功
    setGeneratedContent(data.data.content)
  } catch (error) {
    showToast('网络连接异常,请检查网络后重试')
  }
}

案例二:多语言错误提示方案

AI 产品出海需要支持多语言,错误提示也需要国际化。核心思路是:后端返回 error.type 作为 i18n key,前端根据用户语言环境渲染对应的翻译文案。

后端不需要关心用户用什么语言,只需要返回稳定的错误类型标识:

{
  "error": {
    "code": 12001,
    "type": "INSUFFICIENT_TOKENS",
    "message": "Token 额度不足",
    "requestId": "req_xyz"
  }
}

前端用 error.type 作为翻译 key:

// en.json
{
  "errors": {
    "INSUFFICIENT_TOKENS": "Insufficient token balance. Please top up to continue.",
    "INVALID_PROMPT": "Prompt cannot be empty or exceed 10,000 characters.",
    "RATE_LIMITED": "Too many requests. Please try again later."
  }
}
// ja.json
{
  "errors": {
    "INSUFFICIENT_TOKENS": "トークン残高が不足しています。チャージして続行してください。",
    "INVALID_PROMPT": "プロンプトは空または10,000文字を超えることはできません。",
    "RATE_LIMITED": "リクエストが多すぎます。後でもう一度お試しください。"
  }
}

这种方式的好处是:后端不需要处理多语言逻辑,翻译工作可以交给产品或运营团队在翻译文件中维护,开发者只需要确保 error.type 稳定不变。


错误处理检查清单

在上线之前,逐项检查以下要点:

  • 所有 API 接口返回统一的错误响应格式(包含 code、type、message、requestId)
  • 正确使用 HTTP Status Code,不把 401 和 403 混用,不把 422 和 400 混用
  • 5xx 错误响应不暴露堆栈、数据库连接串、内部 IP 等敏感信息
  • 业务错误码按模块分段,每个模块有独立的编号空间
  • 4xx 错误不做自动重试,5xx 错误做指数退避重试
  • POST 请求的重试支持幂等性(Idempotency-Key)
  • 所有错误都记录结构化日志,包含 requestId 便于追踪
  • 设置错误率监控和告警,5xx 错误率超过阈值时自动通知
  • 前端错误提示不暴露技术细节,告诉用户「下一步该做什么」
  • 错误提示支持多语言,使用 error.type 作为 i18n key
  • 429 响应包含 Retry-After 头部
  • 设置全局 Error Boundary,防止渲染异常导致白屏
  • 错误码文档完整,每个错误码都有说明和处理建议
  • 敏感数据(密码、API Key、支付信息)不出现在日志中

小结

错误处理是 API 设计中容易被低估但极其重要的一环。一套好的错误处理体系,让用户知道发生了什么、该怎么做,让开发者能快速定位问题、高效调试,让运维团队能实时监控、及时告警。

核心要点回顾:

  1. 错误码设计:按模块分段,数字 + 字符串组合,兼顾机器处理和人类理解
  2. 响应格式:统一结构,必含 code、type、message、requestId,可选 details 提供字段级错误
  3. HTTP Status:正确使用 4xx 和 5xx,不要把 400 当万能筐
  4. 重试策略:4xx 不重试,5xx 指数退避重试,注意幂等性
  5. 前端处理:不暴露技术细节,告诉用户可以做什么,区分错误的严重程度用不同 UI 形式
  6. 国际化:后端返回 error.type 作为 i18n key,翻译交给前端处理

错误处理不是「锦上添花」的功能,它是产品质量的一部分。一个能优雅处理错误的 API,比一个永远不出错的 API 更让人信赖——因为前者让你知道,即使出了问题,也在掌控之中。


参考资料

  1. RFC 7807 - Problem Details for HTTP APIs — IETF 标准的错误响应格式规范
  2. Errors Best Practices in REST API Design - Speakeasy — REST API 错误处理最佳实践指南
  3. Best Practices for API Error Handling - Postman Blog — Postman 的 API 错误处理综合指南
  4. 浅谈 API 错误码设计 - 知乎专栏 — API 错误码设计思路与实践
  5. 后端接口错误码到底该怎么设计 - 阿里云开发者社区 — 错误码设计的两种方案对比
  6. Best Practices for REST API Error Handling - Baeldung — REST API 错误处理最佳实践
  7. 如何规范 RESTful API 的业务错误处理 - 稀土掘金 — RESTful API 业务错误处理规范
  8. HTTP response status codes - MDN Web Docs — HTTP 状态码完整参考