业务应用的错误分类:统一的 UI 与监控
为业务应用建立错误分类,方便对验证、认证、限流和依赖失败进行统一归类,使告警与 UI 行为保持一致。

错误分类在真实业务应用中解决了什么问题
错误分类是一种共享的错误命名和分组方式,让大家以相同方式处理错误。与其让每个界面和 API 各自发明消息,不如定义一小套类别(比如 validation 或 auth)以及它们在用户界面和监控中显示的规则。
没有这种共享结构,同一个问题会呈现为不同形式。一个必填字段缺失在移动端可能返回“Bad Request”,在网页端显示“出了点问题”,日志中则是一段堆栈跟踪。用户不知道下一步该做什么,而值班团队则要浪费时间猜测这是用户操作错误、攻击还是服务中断。
目标是保持一致性:同一类错误应导致相同的 UI 行为和告警行为。验证问题应指向精确字段。权限问题应阻止操作并说明缺失的访问权限。依赖失败应提供安全的重试方案,同时监控要触发正确的告警。
一个现实例子:销售代表尝试创建客户记录,但支付服务宕机。如果你的应用返回通用的 500,他们会重复提交,之后可能产生重复记录。有了清晰的依赖失败类别,UI 可提示服务暂不可用、防止重复提交,监控也会通知正确的团队。
当一个后端为多个客户端提供服务时,这种一致性尤为重要。如果 API、网页、移动和内部工具都使用相同的类别和代码,失败就不再显得零散。
一个简单模型:类别、代码、消息、细节
当你把常混在一起的四件事分开时,分类更容易维护:类别(问题类型)、代码(稳定标识符)、消息(面向人的文本)和细节(结构化上下文)。HTTP 状态仍然重要,但不应承载全部含义。
类别回答:“UI 和监控应如何表现?”一个 403 在某处可能表示“auth”,在另一个场景下如果后来加了策略规则则可能是“policy”。类别关乎行为,而非传输层。
代码回答:“到底发生了什么?”代码应稳定且平淡。重命名按钮或重构服务后,代码不应改变。仪表盘、告警和支持脚本都依赖它们。
消息回答:“我们告诉用户什么?”要决定消息的受众。面向用户的消息应简短且友好;面向支持的消息可以包含下一步操作。日志可更技术性一些。
细节回答:“我们需要什么来修复它?”保持细节结构化,便于 UI 响应。对于表单错误,细节可能是字段名;对于依赖问题,细节可能是上游服务名和 retry-after 值。
很多团队常用一个紧凑的形状:
{
"category": "validation",
"code": "CUSTOMER_EMAIL_INVALID",
"message": "Enter a valid email address.",
"details": { "field": "email", "rule": "email" }
}
随着功能变化,保持类别少且稳定,新增代码而不是复用旧码。这样 UI 行为、监控趋势和支持流程在产品演进时仍然可靠。
核心类别:validation、auth、rate limits、dependencies
大多数业务应用可以从四个经常出现的类别开始。如果在后端、网页和移动端统一命名和处理,UI 可以一致响应,监控也更可读。
Validation(预期内)
当用户输入或业务规则不满足时会产生验证错误。这是正常情况,应易于修复:必填字段缺失、格式不对,或像“折扣不能超过 20%”或“订单总额必须 > $0”这样的规则。UI 应高亮具体字段或规则,而不是显示通用警告。
认证与授权(预期内)
Auth 错误通常分为两类:未认证(未登录、会话过期、令牌缺失)和未授权(已登录但缺权限)。要区别对待。第一种适合用“请重新登录”。对第二种,避免泄露敏感细节,但仍应明确:"您没有权限批准发票。"
限流(预期,但有时间维度)
限流表示“请求过多,请稍后重试”。它常在导入、大量仪表盘请求或重复重试时出现。应包含 retry-after 提示(即使只是“等待 30 秒”),并让 UI 退避而不是不断打服务器。
依赖失败(通常是意外)
依赖失败来自上游服务、超时或故障:支付提供商、邮件/SMS、数据库或内部服务。用户无法自己修复这些问题,所以 UI 应提供安全的替代方案(保存草稿、稍后重试、联系支持)。
关键差别在于行为:预期内的错误是正常流程的一部分,需要精确反馈;意外错误表示不稳定,应触发告警、关联 ID 并慎重记录日志。
逐步实践:在一次研讨会中建立你的分类
分类应足够小以便记住,但又足够严格以确保两队能以相同方式标记同一问题。
1)限定时间并选一小套类别
从 60–90 分钟的工作坊开始。列出你最常见的错误(错误输入、登录问题、请求过多、第三方故障、意外 Bug),然后把它们合并成 6–12 个类别,保证每个人不用查文档也能说出来。
2)达成稳定的代码命名规则
选一个在日志和工单中仍易读的命名模式。保持简短,避免版本号,并把代码视为发布后的永久项。一个常见模式是类别前缀加清晰的短语,如 AUTH_INVALID_TOKEN 或 DEP_PAYMENT_TIMEOUT。
离开会议前,决定每个错误必须包含的内容:类别、代码、安全消息、结构化细节,以及 trace 或 request ID。
3)为类别与代码写一条规则
当类别变成垃圾箱会出现问题。一条简单规则有帮助:类别回答“UI 与监控应如何反应?”,代码回答“具体发生了什么?”。如果两个失败需要不同的 UI 行为,就不应归到同一类别。
4)为每个类别设定默认 UI 行为
决定默认情况下用户看到什么。验证高亮字段;认证引导登录或显示访问消息;限流显示“请在 X 秒后重试”;依赖失败展示冷静的重试界面。一旦存在这些默认,新增功能就能遵循它们,而不是发明一次性的处理。
5)用真实场景测试
跑五个常见流程(注册、结账、搜索、管理编辑、文件上传),并标注每次失败。如果团队争论不休,通常需要一条更清晰的规则,而不是再加二十个代码。
验证错误:让用户能立即采取行动
验证是那类你通常希望立即展示的失败。它应具有可预测性:告诉用户如何修复,且不会触发重试循环。
字段级和表单级验证是不同的问题。字段级错误对应单个输入(邮箱、电话、金额)。表单级错误涉及输入组合(开始时间必须早于结束时间)或缺少前置条件(未选择配送方式)。你的 API 响应应清楚区分,便于 UI 做出正确反应。
常见的业务规则失败比如“超出信用额度”。用户可能输入了合法数值,但因账户状态不允许此操作。把它当作表单级验证错误,给出明确原因和安全提示,例如“您的可用额度为 $500。请减少金额或申请额度提升。”避免暴露内部字段名、评分模型或规则引擎细节。
一个可操作的响应通常包括稳定代码(而不仅仅是英文句子)、面向用户的友好消息、用于字段级问题的可选字段指针,以及小而安全的提示(格式示例、允许范围)。如果工程师需要规则名,把它放到日志中,而不是 UI。
以不同方式记录验证失败而不是系统错误。记录足够的上下文以便发现模式但不要存储敏感数据。记录用户 ID、请求 ID、规则名或代码以及失败字段。对于值,只记录必要信息(通常是“存在/缺失”或长度)并掩码敏感内容。
在 UI 中,注重修复而不是重试。高亮字段、保留用户输入、滚动到第一个错误并禁用自动重试。验证错误不是临时的,“再试一次”只会浪费时间。
认证与权限错误:兼顾安全与清晰
认证与授权失败对用户看起来类似,但在安全、UI 流程和监控上含义不同。将它们分开能让各端行为在 web、移动和 API 客户端间一致。
未认证表示应用无法确认用户身份。常见原因是凭据缺失、令牌无效或会话过期。Forbidden(禁止)表示用户已知但无权执行该操作。
会话过期是最常见的边缘情况。如果支持刷新令牌,先尝试一次静默刷新并重试原始请求。如果刷新失败,返回未认证错误并将用户引导至登录。避免循环:刷新尝试一次后若失败就停止并明确下一步。
UI 行为应保持可预测:
- 未认证:提示登录并保留用户正在做的操作
- 禁止:停留在当前页并显示访问提示,同时提供安全操作如“申请访问”
- 账户被禁用或权限被撤销:登出并显示简短提示,告知可联系支持
为审计记录,记录足够信息以回答“谁尝试了什么,为什么被阻止”,但不要暴露机密。一个有用的记录包含用户 ID(如果已知)、租户或工作区、操作名、资源标识、时间戳、请求 ID 和策略检查结果(允许/拒绝)。把原始令牌和密码排除在日志之外。
面向用户的消息不要泄露角色名、权限规则或内部策略结构。“您没有权限批准发票”比“仅 FinanceAdmin 可以批准发票”更安全。
限流错误:在高载下表现可预测
限流不是 bug,而是安全保护。把它作为一级类别,这样 UI、日志和告警在流量激增时都能一致反应。
限流通常有几种形态:按用户(单人点击过快)、按 IP(多个用户在同一办公网络)、或按 API key(某个集成作业失控)。原因不同,修复方式也不同。
一个良好限流响应应包含的内容
客户端需要两件事:知道自己被限流了,以及何时可以重试。返回 HTTP 429 并给出明确等待时间(比如 Retry-After: 30)。同时包含稳定错误代码(如 RATE_LIMITED),便于仪表盘分组。
用冷静且具体的文案。"Too many requests" 虽然技术上正确,但没帮助。"请等待 30 秒再试" 能设定预期并减少重复点击。
在 UI 端,阻止快速重试。常见模式是禁用操作直到等待期结束,展示简短倒计时,然后提供一次安全重试。避免让用户认为数据丢失。
监控方面不要对每个 429 都报警。跟踪速率并在异常峰值时告警:某个端点、租户或 API key 的突增是可操作的。
后端行为也要可预测。自动重试使用指数退避,并使重试幂等。像“创建发票”这类操作如果第一次请求实际成功就不应创建两次。
依赖失败:在故障时避免混乱
依赖失败是用户无法通过更好输入来解决的那类问题。用户做对了事情,但支付网关超时、数据库连接中断或上游返回 5xx。把它们作为独立类别,这样 UI 和监控都能一致反应。
先为常见失败类型命名:超时、连接错误(DNS、TLS、连接被拒绝)和上游 5xx(Bad Gateway、Service Unavailable)。即便无法得知根本原因,也能记录发生了什么并一致响应。
重试还是快速失败
重试对短暂故障有用,但也可能加剧故障。使用简单规则让各队伍做出相同判断。
- 当错误很可能是暂时的时重试:超时、连接重置、502/503
- 对于用户引起或永久性的问题快速失败:依赖返回 4xx、凭据无效、资源不存在
- 限制重试次数(例如 2–3 次)并加入小退避
- 除非有幂等键,否则不要对非幂等操作重试
UI 行为与安全替代方案
当依赖失败时,告诉用户下一步可做什么且不怪罪用户:"临时问题,请稍后再试"。如果有安全替代方案就提供。例如:Stripe 宕机时,允许用户将订单保存为“待付款”并发送邮件确认,而不是丢失购物车。
还要保护用户免受双重提交。如果用户在响应缓慢时连点两次 "Pay",系统应能检测到。对创建并扣款的流程使用幂等键,或在再次执行前检查状态如“订单已支付”。
对监控而言,记录能快速回答的问题字段:"哪个依赖失败,严重到什么程度?" 捕捉依赖名、端点或操作、耗时以及最终结果(timeout、connect、upstream 5xx)。这能让告警和仪表盘有意义而非噪声。
在各渠道间让监控与 UI 保持一致
只有当每个渠道说同一套语言时,分类才有效:API、网页 UI、移动端和日志。否则同一问题会呈现为五种不同的消息,没人知道这是用户错误还是真正的中断。
把 HTTP 状态码当作次要层。它们对代理和基础客户端行为有用,但你的类别和代码应承载实际含义。依赖超时可能仍返回 503,但类别会告诉 UI 提供“重试”选项,并告诉监控联系值班。
让每个 API 返回统一的错误形状,即便来源不同(数据库、认证模块、第三方 API)。一个简单的形状能让 UI 处理和仪表盘一致:
{
"category": "dependency",
"code": "PAYMENTS_TIMEOUT",
"message": "Payment service is not responding.",
"details": {"provider": "stripe"},
"correlation_id": "9f2c2c3a-6a2b-4a0a-9e9d-0b0c0c8b2b10"
}
Correlation ID 是把“用户看到了错误”与“我们能追踪到它”连接起来的桥梁。在 UI 中显示 correlation_id(提供复制按钮会很有帮助),并在后端日志中记录它以便跨服务追踪请求。
达成一致哪部分信息能在 UI 显示、哪部分只放日志。一个实用的划分是:UI 获取类别、清晰消息和下一步操作;日志获取技术细节和请求上下文;两者共享 correlation_id 与稳定错误代码。
一套快速检查清单,确保错误系统一致
一致性是最好的那种无聊:每个渠道行为一致,监控说的也是真相。
先检查后端,包括后台作业和 webhook。如果任何字段是可选的,人们就会跳过它,致使一致性破坏。
- 每个错误包含类别、稳定代码、对用户安全的消息和 trace ID。
- 验证问题是预期内的,不应触发 paging 告警。
- 认证与权限问题应被记录用于安全模式分析,但不当作中断处理。
- 限流响应包含重试提示(例如等待秒数),并且不触发泛滥的告警。
- 依赖失败包含依赖名以及超时或状态细节。
然后检查 UI 规则。每个类别应映射到一种可预期的屏幕行为,这样用户就不用猜下一步该做什么:验证高亮字段、认证引导登录或显示访问、限流显示冷静等待、依赖失败提供重试与可用的替代方案。
一个简单测试是在预发布环境触发每个类别的错误,确认网页、移动和管理面板显示相同结果。
常见错误与实用的下一步
把错误处理当作事后再做是破坏系统的最快方式。不同团队会使用不同的词、不同的代码和不同的 UI 行为来描述同一问题。当分类工作保持一致时才能带来价值。
常见失败模式:
- 把内部异常文本泄露给用户。它既让人困惑又可能暴露敏感信息。
- 把所有 4xx 都标记为“validation”。缺权限并不等同于字段缺失。
- 每个新功能都随意新增代码而不经审查。你会得到 200 个实际上只代表 5 件事的代码。
- 重试了错误的类型。重试权限错误或错误邮箱只会制造噪声。
一个简单例子:销售代表提交“创建客户”表单并收到 403。如果 UI 把所有 4xx 都当作验证错误,就会高亮随机字段并提示“修正输入”,而不是告诉他们需要权限。监控结果会显示“验证问题”激增,但真实问题是角色配置。
适合一次短会完成的实用下一步:写一页的分类文档(类别、使用场景、5–10 个典型代码)、定义消息规则(用户看到什么 vs 日志里写什么)、为新代码加轻量审查门、按类别设定重试规则,然后实现端到端(后端响应、UI 映射与监控仪表盘)。
如果你用的是 AppMaster (appmaster.io),把这些规则集中管理会有帮助,这样相同的类别和代码行为会贯穿后端、网页与原生移动应用。
常见问题
当同一个后端为多个客户端(网页、移动、内部工具)提供服务,或者支持和值班人员不断问“这是用户操作错误还是系统问题?”时,就值得开始建立错误分类。一旦你有重复的流程(注册、结账、导入、管理编辑),一致的处理方式会很快带来价值。
一个合适的起点是 6–12 个类别,人们不需要查文档就能记住。保持类别稳定且范围较广(比如 validation、auth、rate_limit、dependency、conflict、internal),把具体情形放在代码里而不是不断新增类别。
类别决定行为,代码标识具体情形。类别告诉 UI 和监控要做什么(高亮字段、提示登录、退避、提供重试),而代码在仪表盘、告警和支持脚本中保持稳定,即便 UI 文案变化也能用于分组和自动化。
把消息当作内容而不是标识符。为 UI 返回简短且对用户安全的消息,使用稳定代码进行分组和自动化。如果需要更技术化的文字,把它放到日志中并关联相同的 correlation ID。
每个错误响应应包含:类别、稳定代码、对用户安全的简短消息、结构化的细节以及 correlation 或 request ID。细节应便于客户端采取操作,例如哪个字段失败或需要等待多长时间,但不要把原始异常文本直接回传给客户端。
尽量返回字段级指针,这样 UI 可以高亮具体输入并保留用户已输入的内容。对组合输入或业务规则(如日期范围或信用额度)的错误,使用表单级错误,这样 UI 不会猜测错误字段。
“未登录”表示无法证明用户身份(凭证缺失、令牌无效或会话过期),UI 应将用户引导到登录并保留其任务。“无权限”表示用户已知但无权执行当前操作,UI 应保持在当前页并显示访问提示,避免暴露内部角色或策略细节。
返回明确的等待时间(例如 retry-after 值)并保持代码稳定,以便客户端一致实现退避。在 UI 端禁用重复点击并提供明确的下一步,因为快速自动重试通常会让限流情况更糟。
仅在失败很可能是临时的情况下重试(超时、连接重置、上游 502/503),并用小的退避与重试上限限制重试次数。对于非幂等操作,要求提供幂等键或进行状态检查,否则重试可能在第一次请求已成功的情况下造成重复。
把 correlation ID 显示给用户(便于支持查询),并在服务器端与代码和关键细节一起记录。这样可以跨服务追踪同一次失败。在使用 AppMaster 的项目中,把这种响应格式集中管理有助于让后端、Web 与原生移动端行为保持一致。


