2025年7月28日·阅读约1分钟

业务应用的错误分类:统一的 UI 与监控

为业务应用建立错误分类,方便对验证、认证、限流和依赖失败进行统一归类,使告警与 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_TOKENDEP_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 的突增是可操作的。

后端行为也要可预测。自动重试使用指数退避,并使重试幂等。像“创建发票”这类操作如果第一次请求实际成功就不应创建两次。

依赖失败:在故障时避免混乱

标准化你的错误响应
为你的 API 错误建模结构化细节,让客户端可预测地响应。
创建后端

依赖失败是用户无法通过更好输入来解决的那类问题。用户做对了事情,但支付网关超时、数据库连接中断或上游返回 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 与稳定错误代码。

一套快速检查清单,确保错误系统一致

冷静处理限流
添加 retry-after 和退避逻辑一次实现,让所有客户端一致处理 429。
试用 AppMaster

一致性是最好的那种无聊:每个渠道行为一致,监控说的也是真相。

先检查后端,包括后台作业和 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。

每个 API 错误响应应包含什么?

每个错误响应应包含:类别、稳定代码、对用户安全的简短消息、结构化的细节以及 correlation 或 request ID。细节应便于客户端采取操作,例如哪个字段失败或需要等待多长时间,但不要把原始异常文本直接回传给客户端。

我们如何让验证错误具有可操作性而非泛泛而谈?

尽量返回字段级指针,这样 UI 可以高亮具体输入并保留用户已输入的内容。对组合输入或业务规则(如日期范围或信用额度)的错误,使用表单级错误,这样 UI 不会猜测错误字段。

我们应如何处理“未登录”与“无权限”错误?

“未登录”表示无法证明用户身份(凭证缺失、令牌无效或会话过期),UI 应将用户引导到登录并保留其任务。“无权限”表示用户已知但无权执行当前操作,UI 应保持在当前页并显示访问提示,避免暴露内部角色或策略细节。

实现限流错误的正确方式是什么?

返回明确的等待时间(例如 retry-after 值)并保持代码稳定,以便客户端一致实现退避。在 UI 端禁用重复点击并提供明确的下一步,因为快速自动重试通常会让限流情况更糟。

什么时候应重试依赖失败,如何避免重复?

仅在失败很可能是临时的情况下重试(超时、连接重置、上游 502/503),并用小的退避与重试上限限制重试次数。对于非幂等操作,要求提供幂等键或进行状态检查,否则重试可能在第一次请求已成功的情况下造成重复。

关联 ID 在真实事件中有什么帮助,应出现在何处?

把 correlation ID 显示给用户(便于支持查询),并在服务器端与代码和关键细节一起记录。这样可以跨服务追踪同一次失败。在使用 AppMaster 的项目中,把这种响应格式集中管理有助于让后端、Web 与原生移动端行为保持一致。

容易上手
创造一些 惊人的东西

使用免费计划试用 AppMaster。
准备就绪后,您可以选择合适的订阅。

开始吧