关于 awesome-error-standards
用于设计和审查 API 与应用一致错误处理方式的 Skill,帮助开发者分类故障、定义安全的错误契约,并在不泄露内部信息的前提下便于诊断。
主要功能
- 01区分运行故障与程序错误
- 02定义结构化类型错误模式
- 03将 API 错误映射到 HTTP 状态码
- 04在边界处验证输入
- 05记录带安全上下文的故障
- 06指导重试与静默故障诊断
适用场景
- 01统一 API 错误响应
- 02重构应用中的故障处理
- 03审查重试和日志行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ khasky/ awesome-agent-skills --skill awesome-error-standards安装后在 Claude Code 中输入/awesome-error-standards确认已加载。
附带文件evals/eval_queries.json
详细介绍
错误处理模式
采用一致的抛出、捕获、记录和呈现错误的模式,让故障可预测且便于调试,同时不泄露内部信息。
适用场景
- 在 API 或应用中实现或重构错误处理
- 用户询问“错误处理”“如何处理错误”或“一致的错误”
- 审查代码中的故障处理是否妥当
- 为服务或 API 定义或编写错误契约文档
核心原则
- 区分运行时错误与程序员错误——先分类。运行时故障(网络超时、输入无效、记录缺失)是可预期的:应处理、重试或呈现给用户。程序员错误(访问未定义内容、破坏不变量)属于缺陷:应明确崩溃或重启进程,而不是在状态已损坏的情况下勉强运行。
- 让缺陷无法发生,而不只是修复它——单点修复只能阻止一条路径;纵深防御则能阻止整类问题。优先在进入生产环境前预防,依次采用:(1) 在类型系统中让无效状态无法表示(判别联合、品牌类型/不透明类型),使错误状态无法通过编译;(2) 在边界处进行验证;(3) 使用运行时保护;(4) 使用错误边界。对于高价值不变量,应在多个层级进行保护,并测试绕过外层后内层仍能捕获问题(主动跳过第 1 层,确认第 2 层能捕获;确认 mock 不会悄然禁用验证)。 按阶梯评估每项修复的级别。控制:错误无法发生(类型、API 结构、移除的选项)。警告:错误发生时会立即显现(编译错误、验证失败、lint 规则)。检测:之后才会发现问题(测试、监控、告警)。最低一级只是注释、文档中的一句话或“务必小心”;这不是修复,不计入等级。监控并非预防:如果缺陷已经影响用户,此时才触发告警,只是转移了问题,并未阻止问题。修复报告应说明修复使哪些情况无法发生,以及明确保留了哪些可能性。
- 在启动时失败,而不是在生产环境中失败——初始化时验证所有必需配置、环境变量和连接;验证失败则拒绝启动,而不是等到首次请求时才发现缺少值。
- 尽早失败——尽早验证输入和前置条件;抛出错误或返回带有明确消息的错误。不要在状态无效时继续执行。
- 不要吞掉错误——避免使用空的 catch 块或忽略错误的宽泛 catch。应记录并/或重新抛出错误,或返回结果类型。传播错误时应附带上下文。
- 结构化错误——API 错误和日志应使用一致的结构(例如 code、message、details)。带类型的错误类或错误代码有助于调用方按类型处理。
- 区分用户与开发者——面向用户的消息应安全且可操作;面向开发者的详细信息(堆栈、内部消息、请求 ID)仅应出现在日志或调试信息中。绝不要向最终用户泄露堆栈跟踪或 SQL。
工作流程
- 确定边界——错误从哪里产生(验证、DB、外部 API、身份验证)?在哪里处理(路由处理程序、中间件、顶层)?确保每一层都能处理错误,或附带上下文向上层传播。
- 定义错误结构——对于 API:HTTP 状态码 + 响应体封装(code、message、可选的 details)。对于代码:错误类或结果类型。若项目已有模式,应与之保持一致。
- 将错误映射到 HTTP 状态码(适用于 API)——验证 → 400 或 422;身份验证 → 401;授权 → 403;未找到 → 404;冲突 → 409;服务器错误 → 500。发生错误时,不要返回带有
success: false的 200。为验证失败选择一种风格(400 或 422),记录该约定,并在所有端点统一应用——混用会破坏生成的客户端及调用方的错误分支处理。 - 响应前记录日志——在适当的级别(error/warn)记录附带上下文的错误(请求 ID、安全时可记录用户 ID、操作)。然后返回对用户安全的响应。
- 验证并清理数据——在 API 边界使用 schema(Zod、Pydantic 等);以一致的格式返回字段级验证错误。
API 错误响应
标准封装:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Must be a valid email address", "code": "invalid_format" }
]
}
}
- code — 机器可读(例如
validation_error、not_found、rate_limit_exceeded)。客户端可以据此进行分支处理。 - message — 面向用户的可读信息,可安全地展示给用户。不包含堆栈跟踪或内部路径。
- details — 可选;对于验证错误,列出字段级错误。对于通用 500 错误则省略。
- request_id / trace_id — 可选,放在封装或标头中以供支持排查;不要暴露内部信息。
HTTP 状态码:400 Bad Request、401 Unauthorized、403 Forbidden、404 Not Found、409 Conflict、422 Unprocessable Entity、429 Too Many Requests、500 Internal Server Error。应按语义使用;不要用 200 表示错误。
重试失败的调用(客户端)
根据重试是否有帮助对每种失败进行分类——重试不可重试的错误只会浪费时间,还可能造成重复操作:
- 永不重试(修正请求):400、401、403、404、415、大多数 422——输入或身份验证有误,重试不会改变结果。
- 修改请求后重试:402(充值 / 更改套餐)、部分 422(调整负载)。不要盲目重试。
- 退避后重试:429、500、502、503、504——临时错误。限制尝试次数(约 5 次)和总等待时间(约 30 秒);使用带抖动的指数退避,以避免惊群效应。
规则:
- 遵循服务端的信号——如果存在
Retry-After和X-RateLimit-Reset/X-RateLimit-*标头,应优先遵循它们,而不是使用自己的计时器。 - 对重试的状态变更请求使用 Idempotency-Key——POST/PATCH 请求可能已在服务端成功扣费、创建或发送,但响应却丢失;简单重试会导致重复扣费。发送稳定的幂等键,以便服务端进行去重。
- 流式传输(SSE/websocket)——错误会作为流内事件(
error事件或终止帧)到达,而不是作为 HTTP 状态码,因为流打开时状态码已经是 200。应显式处理流的错误通道,而不只是检查初始响应码。
代码模式
类型化错误(推荐):
class NotFoundError extends Error {
constructor(resource: string) {
super(`${resource} not found`);
this.name = 'NotFoundError';
}
}
// Caller: catch (e) { if (e instanceof NotFoundError) return res.status(404).json(...); }
分层处理:底层抛出错误或返回 Result;路由处理程序或中间件捕获错误,并将其映射为 HTTP 响应并记录日志。不要在每一层都捕获错误;应向上传播,并在边界处处理。
异步:在异步函数中使用 try/catch;确保 Promise 拒绝得到处理(例如通过全局处理程序或 .catch),以便记录日志且不留下未处理的拒绝。为 unhandledRejection/uncaughtException(或平台对应机制)注册边界处理程序:记录带有上下文的信息,然后针对程序错误退出——不要在状态已损坏时继续运行。
错误日志记录
- 级别:需要关注的故障使用 error;可恢复的情况(例如重试、回退)使用 warn。
- 内容:记录消息、错误类型、堆栈(仅限服务端)、request_id。不要记录密钥或完整的 PII。
- 只记录一次:在处理错误的边界处记录日志;避免在每一层重复记录同一错误。
- 安全序列化:在日志 / 错误处理路径中使用安全序列化器(
safeStringify、结构化日志记录器自带的序列化器),不要直接使用JSON.stringify——它会因循环引用和BigInt而抛出异常,甚至导致尝试报告故障的处理程序自身崩溃。
规则
- 遵循项目中现有的模式(错误类、响应结构、日志记录)。如果没有现成模式,则引入最精简且一致的模式,并将其记录下来。
- 不要向最终用户暴露内部细节(堆栈跟踪、数据库错误、文件路径);仅在服务端记录这些信息。
- 验证错误:返回带有字段级详情的 400/422,而不是 500。
静默失败
这是最棘手的一类:一开始运行正常,返回 200,却悄无声息地什么都没做(或做错了)。按顺序诊断:组件未接线 / 未注册 → 未定义引用被解析为空操作 → 类型 / 信号不匹配导致负载丢失 → 错误被吞掉(catch {}、|| true)→ 上游采样 / 过滤导致下游指标悄然出错。切勿因某个键被拒绝就断定功能不存在——发布降级配置之前,先查找该组件自己的键;如果替换了工具 / 方法,应明确说明,而不是报告原有功能缺失。
检查清单
- 在边界处验证输入;以标准格式返回验证错误
- 将错误映射到正确的 HTTP 状态码(错误时不要返回 200)
- 面向用户的消息应安全且可操作;详细信息仅记录在日志中
- 响应前记录错误及上下文(请求 ID、操作)
- 不要留空的 catch;应处理错误、记录并重新抛出,或返回结果类型
反模式
| 反模式 | 更好的做法 |
|---|---|
使用 { success: false, error: "..." } 返回 200 OK | 使用正确的 HTTP 状态码(4xx/5xx)和错误正文 |
| 空的 catch 块 | 记录并重新抛出,或返回 Err(result);如果有意忽略,则说明原因 |
丢弃错误:Go 中的 _ := f()、Python 中的裸 except:、未经检查的 Close() | 处理错误,或就地说明忽略错误为何安全 |
在库代码中因操作错误调用 panic() / process.exit() | 返回或抛出带类型的错误;由边界决定如何处理 |
| 出错时记录完整请求/响应 | 记录 ID 和摘要;对机密和个人身份信息进行脱敏 |
| 向客户端暴露堆栈跟踪 | 在服务端记录堆栈;向客户端返回通用消息 |
| 各端点的错误格式不同 | 在整个 API 中使用统一的封装格式(code、message、details) |
| 领域错误与传输层相关:服务或存储库返回或抛出框架响应对象或 HTTP 状态码 | 抛出与传输层无关的带类型错误;由唯一的全局处理器将其映射到 HTTP——业务逻辑与交付方式保持解耦,更易于复用和测试 |