agentsclimarketplace

Api error handling

Skill nanami7777777/chinese-fullstack-skills/api-error-handling

Use this skill when implementing error handling in APIs, backend services, or full-stack applications. It provides decision trees for when to use which HTTP status code, how to structure error responses for Chinese teams, how to handle database errors, third-party API failures, validation errors, and authentication errors with proper logging and user-facing messages.From its SKILL.md

Install
npx -y skills add nanami7777777/chinese-fullstack-skills --skill api-error-handling

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

8.8 KB, ~2.6k tokens by cl100k_base, as published. Nobody here has run it

API 错误处理决策指南

这个 skill 不是教你"什么是错误处理",而是帮你在具体场景下做出正确的决策。

核心原则

  1. 对外友好,对内详细 — 用户看到的是"操作失败,请稍后重试",日志里记的是完整的错误栈和上下文
  2. 错误要可追踪 — 每个错误响应带一个 traceId,用户反馈时能快速定位
  3. 不要吞掉错误catch (e) {} 是最大的罪恶
  4. 区分可恢复和不可恢复 — 网络超时可以重试,数据不一致不能

HTTP 状态码决策树

遇到错误时,按这个顺序判断用哪个状态码:

请求有问题吗?
├── 参数格式错误 → 400 Bad Request
├── 没有登录 → 401 Unauthorized
├── 登录了但没权限 → 403 Forbidden
├── 资源不存在 → 404 Not Found
├── 资源已存在(如邮箱重复) → 409 Conflict
├── 请求体太大 → 413 Payload Too Large
├── 请求太频繁 → 429 Too Many Requests
└── 参数合法但业务规则不允许 → 422 Unprocessable Entity
    例:余额不足、库存不够、已过截止时间

服务端有问题吗?
├── 代码 bug → 500 Internal Server Error
├── 依赖服务挂了 → 502 Bad Gateway
├── 服务过载 → 503 Service Unavailable
└── 依赖服务超时 → 504 Gateway Timeout

最常犯的错误

✗ 所有错误都返回 200 + { code: -1 }     ← 前端无法用 HTTP 拦截器统一处理
✗ 所有错误都返回 500                     ← 客户端无法区分是自己的问题还是服务端的问题
✗ 404 和 403 混用                       ← 安全问题:403 泄露了资源存在的信息
✗ 业务错误用 400                        ← 400 是"请求格式错误",不是"业务规则不允许"

错误响应格式

// 统一错误响应结构
interface ErrorResponse {
  code: number          // 业务错误码(不是 HTTP 状态码)
  message: string       // 用户可见的错误信息(中文)
  traceId: string       // 追踪 ID,用于日志关联
  errors?: FieldError[] // 字段级错误(仅参数验证时)
}

interface FieldError {
  field: string         // 字段名
  message: string       // 该字段的错误信息
}

业务错误码设计

错误码 = HTTP状态码前缀 + 模块编号 + 序号

模块编号:
  01 = 用户
  02 = 订单
  03 = 支付
  04 = 商品
  05 = 通知

示例:
  400101 = 用户模块参数错误(用户名格式不对)
  401011 = 用户模块未登录
  403012 = 用户模块无权限
  404021 = 订单模块订单不存在
  409011 = 用户模块邮箱已注册
  422031 = 支付模块余额不足

具体场景处理

场景 1:数据库错误

async function createUser(data: CreateUserInput) {
  try {
    return await prisma.user.create({ data })
  } catch (error) {
    // 唯一约束冲突 → 409
    if (error.code === 'P2002') {
      const field = error.meta?.target?.[0] ?? 'unknown'
      throw new AppError(409, `${field} 已存在`, { field })
    }

    // 外键约束失败 → 422
    if (error.code === 'P2003') {
      throw new AppError(422, '关联数据不存在')
    }

    // 连接失败 → 503(不是 500,因为是依赖问题不是代码 bug)
    if (error.code === 'P1001') {
      logger.error('数据库连接失败', { error })
      throw new AppError(503, '服务暂时不可用,请稍后重试')
    }

    // 其他未知数据库错误 → 500
    logger.error('未知数据库错误', { error, input: data })
    throw new AppError(500, '操作失败,请稍后重试')
  }
}

场景 2:第三方 API 调用失败

async function sendSMS(phone: string, code: string) {
  try {
    const result = await smsProvider.send(phone, code)
    if (!result.success) {
      // 第三方返回业务错误(如手机号格式不对)→ 透传给用户
      throw new AppError(422, result.message)
    }
    return result
  } catch (error) {
    if (error instanceof AppError) throw error

    // 网络超时 → 可重试
    if (error.code === 'ETIMEDOUT' || error.code === 'ECONNREFUSED') {
      logger.warn('短信服务超时,准备重试', { phone, error: error.message })
      // 重试一次
      try {
        return await smsProvider.send(phone, code)
      } catch (retryError) {
        logger.error('短信服务重试失败', { phone, error: retryError })
        throw new AppError(503, '短信发送失败,请稍后重试')
      }
    }

    // 其他错误
    logger.error('短信发送未知错误', { phone, error })
    throw new AppError(500, '短信发送失败')
  }
}

场景 3:参数验证错误

// 用 Zod 验证,统一转换为 400 + 字段级错误
function validateRequest<T>(schema: ZodSchema<T>, data: unknown): T {
  const result = schema.safeParse(data)
  if (!result.success) {
    const errors = result.error.issues.map(issue => ({
      field: issue.path.join('.'),
      message: issue.message,
    }))
    throw new ValidationError(errors)
  }
  return result.data
}

// 中间件统一处理
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  const traceId = req.headers['x-trace-id'] as string ?? generateTraceId()

  if (err instanceof ValidationError) {
    return res.status(400).json({
      code: 400000,
      message: '参数验证失败',
      traceId,
      errors: err.errors,
    })
  }

  if (err instanceof AppError) {
    return res.status(err.httpStatus).json({
      code: err.code,
      message: err.message,
      traceId,
    })
  }

  // 未知错误 — 记录完整信息,但不暴露给用户
  logger.error('未处理的错误', {
    traceId,
    error: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
    body: req.body,
    userId: req.user?.id,
  })

  res.status(500).json({
    code: 500000,
    message: '服务器内部错误,请稍后重试',
    traceId,
  })
}

场景 4:并发冲突

// 乐观锁处理并发更新
async function updateOrderStatus(orderId: string, newStatus: string, version: number) {
  const result = await prisma.order.updateMany({
    where: { id: orderId, version },
    data: { status: newStatus, version: version + 1 },
  })

  if (result.count === 0) {
    // 版本号不匹配 → 被其他请求先更新了
    throw new AppError(409, '订单状态已变更,请刷新后重试')
  }
}

前端错误处理配合

// axios 拦截器 — 统一处理后端错误
axios.interceptors.response.use(
  (response) => response.data,
  (error) => {
    const status = error.response?.status
    const data = error.response?.data

    switch (status) {
      case 400:
        // 参数错误 — 显示字段级错误
        if (data.errors?.length) {
          // 交给表单组件显示
          return Promise.reject({ type: 'validation', errors: data.errors })
        }
        message.error(data.message ?? '请求参数错误')
        break

      case 401:
        // 未登录 — 跳转登录页
        userStore.logout()
        router.push('/login')
        break

      case 403:
        message.error('没有权限执行此操作')
        break

      case 409:
        // 冲突 — 提示用户刷新
        message.warning(data.message ?? '数据已变更,请刷新页面')
        break

      case 422:
        // 业务规则不允许 — 直接显示后端消息
        message.error(data.message)
        break

      case 429:
        message.warning('操作太频繁,请稍后再试')
        break

      case 500:
      case 502:
      case 503:
        message.error('服务暂时不可用,请稍后重试')
        // 可以上报 traceId 方便排查
        if (data.traceId) {
          console.error(`错误追踪 ID: ${data.traceId}`)
        }
        break

      default:
        message.error('网络请求失败')
    }

    return Promise.reject(error)
  }
)

日志规范

// 错误日志必须包含的信息
logger.error('描述发生了什么', {
  traceId,           // 追踪 ID
  userId,            // 谁触发的
  action: '创建订单', // 在做什么操作
  input: { ... },    // 输入参数(脱敏后)
  error: err.message,// 错误信息
  stack: err.stack,  // 错误栈(仅 500 错误)
})

// 脱敏规则
// 手机号:138****1234
// 身份证:110***********1234
// 银行卡:6222 **** **** 1234
// 密码:永远不记录

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.