如何处理接口错误
错误处理的好坏,直接影响用户体验和调试效率
很多开发者在写 API 的时候,把大部分精力放在「正常流程」上——参数校验通过、数据库查询成功、业务逻辑跑通、返回 200 OK。至于错误处理,往往是随手写一个 res.status(500).json({ error: 'Something went wrong' }),觉得够用就行。
这种态度在个人项目里或许没问题,但当你的 AI 产品开始面向海外用户,当第三方开发者通过 API 集成你的服务,当线上出现偶发故障需要快速定位时,粗糙的错误处理会变成一场噩梦。用户看到「Something went wrong」不知道该怎么办,开发者对着没有错误码的响应没法自动重试,运维团队拿着一堆 500 日志分不清是哪个环节出了问题。
好的错误处理不是一次性工作,而是一套完整的设计体系:从错误码的编码规则,到响应格式的统一规范,到 HTTP Status Code 的正确使用,到前端的用户友好提示,到后端的日志记录和监控告警。这些环节缺一不可。
本文会系统讲清楚六个方面:
- 错误码设计原则——怎么编码才有意义、可扩展
- 错误响应格式——统一结构应该包含哪些字段
- 常见错误类型——4xx 客户端错误、5xx 服务端错误、业务错误的区分
- 错误处理最佳实践——日志、监控、重试、降级的工程策略
- 前端错误处理——怎么把技术错误翻译成用户能理解的提示
- 完整案例——从错误码定义到前端展示的全链路实现
错误码设计原则
错误码是错误处理体系的基石。一个设计良好的错误码体系,能让开发者看到错误码就知道问题出在哪里、应该怎么处理,而不需要去翻阅文档。
设计错误码的三个核心原则
1. 标准化
错误码应该遵循某种标准或约定,而不是凭感觉随意编号。最常见的做法是基于 HTTP Status Code 做业务层扩展。HTTP Status Code 解决了「协议层」的错误分类(4xx 是客户端问题,5xx 是服务端问题),但不够精确——400 Bad Request 可能对应十几种不同的参数校验失败。你需要在 HTTP Status Code 之上加一层业务错误码,让调用方能准确定位问题。
2. 可扩展
错误码体系要能随业务增长而扩展,不能出现「错误码用完了」或者「新增一个模块就要重新编号」的情况。推荐的做法是在错误码中嵌入模块标识,比如用 10001 表示用户模块的第一个错误,20001 表示订单模块的第一个错误。这样每个模块有独立的编号空间,互不干扰。
3. 有意义
错误码本身应该能帮助定位问题,而不是一个纯随机数。AUTH_001 比 E10086 更容易理解。但也不建议用纯字符串做错误码——字符串没有数值比较能力,在一些编程语言里处理起来也不如数字方便。比较好的折中方案是:数字错误码 + 字符串错误类型(error code + error type),两者一起返回。
错误码分段设计
| 错误码范围 | 错误类型 | 说明 |
|---|---|---|
| 10000-10999 | 通用错误 | 参数校验、认证授权、限流等公共错误 |
| 11000-11999 | 用户模块 | 注册、登录、个人信息相关错误 |
| 12000-12999 | AI 能力模块 | 模型调用、Token 额度、生成任务相关错误 |
| 13000-13999 | 订单与支付 | 订阅、扣费、退款相关错误 |
| 14000-14999 | 内容与资源 | 文件上传、素材管理、内容审核相关错误 |
| 15000-15999 | 第三方集成 | Webhook、OAuth、外部 API 调用相关错误 |
错误码设计对比
| 维度 | 纯数字方案 | 纯字符串方案 | 数字 + 字符串组合(推荐) |
|---|---|---|---|
| 示例 | 10001 | USER_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
}
}
}错误响应字段说明
| 字段 | 类型 | 是否必需 | 说明 |
|---|---|---|---|
code | number | 是 | 业务错误码,用于程序化判断 |
type | string | 是 | 错误类型标识,可作为 i18n key |
message | string | 是 | 人类可读的错误描述 |
requestId | string | 推荐 | 请求唯一 ID,用于日志追踪 |
details | object | 可选 | 错误详情,如字段级别的校验错误 |
timestamp | string | 可选 | 错误发生时间(ISO 8601 格式) |
path | string | 可选 | 请求路径 |
helpUrl | string | 可选 | 帮助文档链接 |
两种格式对比
| 维度 | 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 | 业务错误码 | 是否可重试 | 前端处理策略 |
|---|---|---|---|---|
| 参数校验失败 | 422 | 10001-10099 | 否,需要修改参数 | 展示字段级别的错误提示 |
| 认证失败 | 401 | 10101-10199 | 否,需要重新登录 | 跳转登录页 |
| 权限不足 | 403 | 10201-10299 | 否,需要升级权限 | 展示权限不足提示 |
| 资源不存在 | 404 | 10301-10399 | 否,资源确实不存在 | 展示 404 页面 |
| 请求频率限制 | 429 | 10401-10499 | 是,按 Retry-After 等待 | 提示稍后再试 |
| Token 额度不足 | 200/422 | 12001-12099 | 否,需要充值 | 引导到充值页面 |
| AI 模型调用失败 | 200/502 | 12101-12199 | 是,自动重试 | 展示加载状态或降级提示 |
| 服务内部异常 | 500 | 99001-99999 | 是,自动重试 | 展示通用错误提示 |
错误处理最佳实践
错误处理不只是「返回一个错误响应」这么简单。一个成熟的错误处理体系,需要覆盖日志记录、监控告警、自动重试和降级策略四个层面。
日志记录
每一次错误都应该被记录,但记录的粒度和内容需要讲究:
- 结构化日志:使用 JSON 格式记录日志,包含
timestamp、level、requestId、error.code、error.type、error.message、stack(仅内部日志)、userId、path、method等字段。结构化日志可以被日志系统(如 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 流程图展示完整的错误处理流程——从请求发出到最终响应,每一步的错误如何处理:
实战案例
案例一: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 设计中容易被低估但极其重要的一环。一套好的错误处理体系,让用户知道发生了什么、该怎么做,让开发者能快速定位问题、高效调试,让运维团队能实时监控、及时告警。
核心要点回顾:
- 错误码设计:按模块分段,数字 + 字符串组合,兼顾机器处理和人类理解
- 响应格式:统一结构,必含 code、type、message、requestId,可选 details 提供字段级错误
- HTTP Status:正确使用 4xx 和 5xx,不要把 400 当万能筐
- 重试策略:4xx 不重试,5xx 指数退避重试,注意幂等性
- 前端处理:不暴露技术细节,告诉用户可以做什么,区分错误的严重程度用不同 UI 形式
- 国际化:后端返回 error.type 作为 i18n key,翻译交给前端处理
错误处理不是「锦上添花」的功能,它是产品质量的一部分。一个能优雅处理错误的 API,比一个永远不出错的 API 更让人信赖——因为前者让你知道,即使出了问题,也在掌控之中。
参考资料
- RFC 7807 - Problem Details for HTTP APIs — IETF 标准的错误响应格式规范
- Errors Best Practices in REST API Design - Speakeasy — REST API 错误处理最佳实践指南
- Best Practices for API Error Handling - Postman Blog — Postman 的 API 错误处理综合指南
- 浅谈 API 错误码设计 - 知乎专栏 — API 错误码设计思路与实践
- 后端接口错误码到底该怎么设计 - 阿里云开发者社区 — 错误码设计的两种方案对比
- Best Practices for REST API Error Handling - Baeldung — REST API 错误处理最佳实践
- 如何规范 RESTful API 的业务错误处理 - 稀土掘金 — RESTful API 业务错误处理规范
- HTTP response status codes - MDN Web Docs — HTTP 状态码完整参考