前端需统一拦截的异常(适用于js)

293 阅读26分钟

基础知识

在现代前端应用的开发中,处理 API 请求的异常是确保系统健壮性和用户体验的关键环节。

然而,许多项目采用分散式错误处理,即在每个发起请求的业务组件内部使用 try/catch 语句来捕获和处理异常。这种模式在小型项目中或许尚可接受,但随着应用规模的扩大,其弊端将日益凸显,最终成为技术债务的重灾区。

分散式处理的弊端

分散式错误处理的主要问题在于其固有的重复性和不一致性。开发者需要在应用的各个角落编写相似的错误处理逻辑,这不仅导致了大量的代码冗余,也使得全局策略的实施变得异常困难 。例如,当需要统一更新错误提示的文案、引入全局的错误日志上报系统,或者实现一个复杂的认证令牌刷新机制时,分散的逻辑点意味着需要对几十甚至上百个文件进行修改,这极大地增加了维护成本和引入新错误的风险。此外,不同开发者或团队实现的处理方式可能存在差异,导致用户在应用的不同部分遇到不一致的错误提示和交互体验,损害了产品的专业性和整体感。

拦截器的引入

为了解决上述问题,架构上必须引入一个集中式的错误处理层。请求拦截器(Interceptor)模式为此提供了完美的解决方案 。拦截器本质上是网络请求的中间件,允许我们在请求被发送前(请求拦截器)或响应被处理后(响应拦截器)对其进行全局性的捕获和操作 。诸如 Axios 这样的流行 HTTP 客户端库,原生提供了强大的拦截器 API,使其成为实现该模式的首选工具 。

通过拦截器,我们可以创建一个唯一的“关卡”,所有出入应用的 API 流量都必须经过这里。这个中心化的控制点赋予了我们强大的能力,可以统一实施认证令牌的附加与刷新、请求/响应的日志记录、数据格式的转换,以及最重要的——全局统一的异常处理 。

import axios from 'axios';

// 创建 Axios 实例
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 15000, // 设置全局请求超时
});

// 添加响应拦截器
apiClient.interceptors.response.use(
  (response) => {
    // 状态码在 2xx 范围内的任何响应都会触发此函数。
    // 在这里可以处理“成功响应中的业务错误”。
    return response;
  },
  (error) => {
    // 任何超出 2xx 范围的状态码都会触发此函数。
    // 这里是集中处理所有 HTTP 和网络错误的核心位置。
    return Promise.reject(error);
  }
);

export default apiClient;

ps:

尽管原生的 Fetch API 并未内置拦截器机制,但其设计思想同样适用于 Fetch 。

通过“猴子补丁”(Monkey-patching)或创建一个封装函数来包装原生的window.fetch,我们可以模拟出拦截器的行为,从而实现相同的集中式控制 。这种方式虽然需要一些额外的封装代码,但其带来的架构优势——代码的模块化、可维护性和一致性——是完全值得的。

设计标准化的错误对象

在拦截器中捕获到的原始错误对象形态各异。一个网络连接中断的错误对象(例如,没有 error.response)与一个服务器返回 500 状态码的 HTTP 错误对象(包含 error.response)在结构上截然不同,而一个 200 OK 响应中包含的业务逻辑错误(例如,{ code: 10045, message: "..." })则完全是另一种形式。如果下游的业务逻辑需要针对每一种原始错误结构编写不同的处理代码,那么我们只是将混乱从多个组件转移到了一个集中的拦截器函数中,并未从根本上解决问题。

真正的解决方案是引入一个标准化层。拦截器的首要职责不应是处理错误,而应是规范化错误。无论错误源自何处——是网络层、HTTP 协议层还是应用业务层——拦截器都应将其转换为一个统一、可预测的标准化错误对象。

推荐的 AppError 结构

一个设计良好的标准化错误对象 AppError 应包含以下字段,以满足用户提示、程序处理和调试监控的全部需求:

class AppError extends Error {
  /**
   * @param message 面向用户的、可直接显示的友好错误信息
   * @param statusCode HTTP 状态码 (例如 404, 503),如果可用
   * @param businessCode 后端定义的业务错误码 (例如 10045),如果可用
   * @param isOperational 标记是否为可预期的操作性错误 (如表单验证失败) vs. 真正的程序 bug (如服务器崩溃)。此字段对于错误上报和监控至关重要 [15, 16]。
   * @param originalError 原始错误对象,用于开发人员调试
   */
  constructor(
    public message: string,
    public statusCode?: number,
    public businessCode?: number | string,
    public isOperational: boolean = true,
    public originalError?: any
  ) {
    super(message);
    this.name = 'AppError';
    // 保持正确的堆栈跟踪
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, AppError);
    }
  }
}

通过将所有类型的失败条件都映射到这个 AppError 对象上,整个应用的其余部分(如 UI 组件、状态管理库)就不再需要关心错误发生的具体细节。它们只需与这个统一的、定义良好的接口进行交互,从而极大地简化了后续的错误处理逻辑,并实现了真正的关注点分离。这种抽象是构建一个可维护、可扩展的前端系统的基石。

API 请求异常的全面分类与处理

第一类:客户端与环境故障

这类故障发生在请求实际到达服务器之前,或由于客户端环境的限制导致通信失败。

网络不可用 (离线)

检测navigator.onLine 属性和 online/offline 窗口事件是检测网络状态的最直接方式,可以提供即时的反馈 。

警告与稳健确认:然而,必须认识到 navigator.onLine 的不可靠性。该属性返回 true 仅表示设备连接到了一个局域网(如路由器),但并不保证该网络具备访问互联网的能力 。完全依赖此属性会导致糟糕的用户体验:应用界面显示在线,但所有网络请求均失败。反之,仅在请求失败后才提示用户离线,又显得反应迟钝。

因此,最佳实践是采用一种两阶段离线检测策略

  1. 阶段一(主动提示) :监听 window 对象的 offline 事件。一旦触发,立即在界面上展示一个非阻塞式的提示条,例如“您似乎已离线,请检查网络连接”。这是一种即时反馈,作为一种“提示”而非“断言”。
  2. 阶段二(明确确认) :在请求拦截器的错误处理部分,如果捕获到的错误是明确的网络错误(例如,在 Axios 中,error.codeENETUNREACHerror.message 为 'Network Error'),此时便可以明确确认应用处于离线状态。在此状态下,可以采取更强的 UI 交互,直接显示一个更醒目的“离线模式”提示。

这种两阶段策略结合了事件监听的即时性和请求失败的准确性,提供了最优的用户体验。

用户提示:“您当前处于离线状态,请检查您的网络连接。”

程序处理

  1. 通过监听 onlineoffline 事件来切换一个全局状态(例如,存储在 Redux 或 Vuex 中)。
  2. 在请求拦截器的错误回调中,检查特定的网络错误代码以确认离线状态。
  3. 在应用处于已确认的离线状态时,阻止发起新的网络请求,以避免不必要的失败和资源消耗。

请求超时

原因:超时通常分为两种。连接超时 (connect timeout) 指在指定时间内未能与服务器建立连接;读取超时 (read timeout) 指连接已建立,但服务器在指定时间内未能返回响应数据 。区分这两者对于性能监控和问题诊断非常有帮助。

配置:应在 Axios 实例的全局配置中设定一个合理的 timeout 值 。根据行业实践,15000 毫秒(15 秒)是一个比较通用的默认值,但具体数值应根据应用的实际场景进行调整 。

用户提示:“服务器响应超时,请稍后重试。”

程序处理

  1. 在拦截器中捕获特定于超时的错误码(例如,Axios 中的 ECONNABORTEDerror.message 包含 'timeout')。
  2. 将超时事件连同请求的 URL 和参数上报到日志系统,这对于监控和发现后端性能瓶颈至关重要。
  3. 在界面上可以提供一个手动重试按钮。请求超时是后续将讨论的自动重试机制的候选场景之一。

跨域 (CORS) 错误

原因:跨域资源共享(CORS)错误并非真正的网络或服务器故障,而是浏览器的一种安全机制 。当一个域的前端代码尝试请求另一个域的资源时,如果服务器的响应头中缺少必要的Access-Control-Allow-Origin 等 CORS 相关头部,浏览器会出于安全考虑主动阻止该请求,并在控制台抛出 CORS 错误。

检测:对于前端 JavaScript 代码来说,CORS 错误是出了名的“不透明” 。在拦截器中,通常只能捕获到一个通用的网络错误(Network Error),其 error.responseundefinedstatus0null。错误的详细原因只会显示在浏览器的开发者工具控制台中,代码层面无法获取。

用户提示:“与服务器通信时发生配置错误,如果问题持续存在,请联系技术支持。” 关键在于,绝不能向普通用户展示“CORS”或“跨域”等技术术语。

程序处理

  1. 前端无法直接修复 CORS 错误,它必须由后端参与配置。
  2. 前端拦截器的职责是:
    • 捕获这个通用的网络错误。
    • 向日志系统上报一条详细的、面向开发者的日志,明确指出“可能是 CORS 配置问题,请检查服务器响应头”。
    • 向用户展示一个通用的、非技术性的失败提示。这可以防止泄露实现细节,并避免给用户带来不必要的困惑。

用户主动取消请求

机制:现代前端开发中,取消请求的标准方式是使用 AbortController API,它被 Fetch 和新版本的 Axios (v0.22.0+) 所支持 。通过创建一个AbortController 实例,并将其 signal 属性传递给请求配置,可以在需要时调用 controller.abort() 来取消正在进行的请求。

使用场景

  1. 当用户在数据加载完成前离开当前页面或组件时,取消该页面的所有请求,以避免不必要的网络流量和可能的状态更新错误。
  2. 在实现搜索框的即时搜索功能时,当用户输入新的关键词后,立即取消上一次的搜索请求。

程序处理

  1. 在拦截器的错误处理逻辑中,必须首先检查错误是否由请求取消引起。可以通过 axios.isCancel(error) (旧版 API) 或检查 error.name === 'AbortError' (新版 AbortController API) 来判断。
  2. 这是一个至关重要的区分:用户主动取消的请求不应被视为一个真正的错误。
  3. 拦截器应该静默地忽略这类错误,不应向用户显示任何提示,也不应将其作为错误上报到监控系统。这样做可以防止日志中充满了预期内的、非故障性的“错误”记录,保持监控数据的纯净。

第二类:HTTP 客户端错误 (4xx 状态码)

4xx 系列状态码表示请求本身存在问题,错误源于客户端 。

400 Bad Request

原因:这通常表示客户端发送的请求存在语法错误或无法被服务器理解。最常见的场景是表单提交时的数据验证失败,例如字段格式不正确、缺少必填项等 。

用户提示:理想情况下,用户提示信息应直接来源于 API 的响应体。一个设计良好的 400 响应会包含结构化的错误信息,指明具体是哪个字段出了什么问题。例如:{ "errors": { "email": "邮箱地址格式不正确" } }。如果 API 仅返回一个通用的 400 错误,则应显示一条通用提示:“您的提交存在问题,请检查所填内容后重试。”

程序处理

  1. 400 错误不应由拦截器进行全局性的弹窗提示。
  2. 拦截器应将完整的错误响应(特别是 error.response.data)传递给发起请求的业务逻辑层(例如,表单组件)。
  3. 由业务组件负责解析响应体中的具体错误信息,并将其显示在对应的表单字段旁边。这为用户提供了最直接、最清晰的反馈,帮助他们快速修正错误。

401 Unauthorized

原因:此状态码表示请求因为缺乏有效身份认证凭证而被拒绝。常见原因包括:未携带认证令牌(Token)、令牌无效或令牌已过期 。服务器无法识别当前用户的身份。

程序处理:简单地将用户登出并重定向到登录页是一种粗暴且用户体验极差的处理方式 。特别是当令牌只是因为过期而失效时,强制用户重新登录会打断其工作流程。

一个现代化的、用户体验友好的系统应将 401 错误视为一个触发无感刷新令牌流程的信号,而非最终的失败。

这个流程是响应拦截器最复杂但也是价值最高的职责之一:

  1. 捕获 401:响应拦截器捕获到 401 错误。
  2. 防止重复刷新:检查一个全局状态标志(例如 isRefreshingToken)。如果该标志为 true,说明已经有一个令牌刷新请求正在进行中。此时,应将当前失败的请求“挂起”(例如,存入一个队列中),等待刷新流程完成。
  3. 发起刷新请求:如果 isRefreshingTokenfalse,则立即将其设置为 true,以阻止后续的 401 错误重复触发刷新。然后,向后端一个专门的刷新令牌接口(例如 /api/auth/refresh)发起请求,通常需要携带一个长期有效的 refreshToken
  4. 刷新成功:刷新接口成功返回了新的访问令牌(accessToken)。拦截器需要:
    • 将新的 accessToken 保存到本地存储(如 localStorage)。
    • 将被 401 拒绝的原始请求(error.config)的认证头更新为新的 accessToken
    • 将全局标志 isRefreshingToken 设置回 false
    • 重新发起原始请求,并处理所有在刷新期间被挂起的其他请求。
    • 对于用户而言,整个过程是无感的,他们可能只会感觉到一次轻微的延迟。
  5. 刷新失败:如果刷新令牌的请求本身也失败了(例如,返回 401403,意味着 refreshToken 也已失效或被撤销),这才是最终的认证失败。此时,程序才应该执行清理操作:清除所有用户相关的本地存储数据,并将用户重定向到登录页面。

用户提示: 在尝试刷新令牌期间,不应向用户显示任何提示。仅当刷新流程最终失败时,才显示提示:“您的登录已过期,请重新登录。”

403 Forbidden

原因:服务器理解了客户端的请求,但拒绝执行。这与 401 的核心区别在于,服务器已经知道了用户的身份(即用户已通过身份验证),但该用户不具备访问所请求资源的权限 。例如,一个普通用户尝试访问管理员才能查看的页面。

用户提示:“您没有权限执行此操作”或“访问被拒绝”。

程序处理

  1. 这是一个明确的、不可恢复的客户端错误。绝不能尝试重试或刷新令牌。
  2. 应将此次访问尝试作为安全事件上报到日志系统,以便进行审计。
  3. 向用户清晰地展示权限不足的信息。根据具体的用户体验设计,可以选择弹出一个提示框,或者将用户重定向到一个安全的“首页”或“无权限”提示页面。
  4. 在架构层面,清晰地区分 401(认证问题)和 403(授权问题)并采取截然不同的处理策略,是构建安全、可靠系统的关键。

404 Not Found

原因:服务器找不到请求的资源。这可能是因为请求的 API 端点不存在,或者请求的某个特定资源(如 GET /users/123 中的用户 123)不存在 。

用户提示:“您请求的资源不存在。”

程序处理

  1. 404 错误的处理方式高度依赖于上下文。
  2. 如果是一次关键的数据加载请求失败(例如,加载页面主体内容),最佳实践通常是引导用户到一个专门设计的 404 页面。
  3. 如果是一个次要的操作(例如,用户点击删除一个项目,但该项目已被他人删除),则一个非阻塞的全局提示(Toast 或 Snackbar)通常就足够了,无需打断用户的整体流程。

429 Too Many Requests

原因:客户端在给定的时间窗口内发送了过多的请求,触发了服务器的速率限制(Rate Limiting)策略 。

用户提示:“您的操作过于频繁,请稍后再试。”

程序处理

  1. 这是一个需要智能化处理的关键场景。拦截器不应只是简单地提示用户。
  2. 必须检查响应头中是否包含 Retry-After 字段 。这个头部会明确告知客户端需要等待多少秒后才能再次尝试。
  3. 如果 Retry-After 头部存在,前端应该严格遵守这个时间。在此期间,可以暂时禁用触发该请求的 UI 元素(如按钮),并在倒计时结束后自动恢复。
  4. 如果 Retry-After 头部不存在,前端应主动实现一个客户端的指数退避(Exponential Backoff)策略(详见第三部分),在允许用户手动重试前施加一个逐渐增长的延迟。

第三类:HTTP 服务器错误 (5xx 状态码)

5xx 系列状态码表明问题出在服务器端,服务器未能完成一个看起来有效的请求 。与 4xx 错误不同,5xx 错误通常是暂时性的,例如服务器正在重启、部署新版本、或遭遇了短暂的网络分区或负载高峰。

正是由于其暂时性的特点,5xx 错误是唯一适合进行自动重试的 HTTP 错误类型 。这一判断是错误处理架构中的一个基本分水岭:4xx 错误应快速失败,而 5xx 错误则应触发重试机制,以增强应用的韧性。

500 Internal Server Error

原因:这是一个通用的、非特定的服务器内部错误,表明服务器遇到了一个意外情况,阻止了它完成请求。这通常是后端应用程序代码中存在未被捕获的 bug 所致 。

用户提示:“服务器发生未知错误,我们的团队已收到通知。请稍后重试。”

程序处理

  1. 将包含完整请求上下文的错误信息上报到日志系统,以便后端团队定位和修复问题。
  2. 立即启动自动重试策略。例如,尝试 2-3 次,每次重试之间采用指数退避加抖动的延迟策略。
  3. 如果所有自动重试均告失败,再向用户显示上述提示信息。

502 Bad Gateway & 504 Gateway Timeout

原因:这两个状态码通常表明问题出在服务器的基础设施层面,而不是应用代码本身。例如,作为网关或代理的服务器(如 Nginx、负载均衡器)从上游服务器收到了无效响应(502),或在规定时间内未能收到上游服务器的响应(504) 。

用户提示:“我们的服务连接暂时出现问题,请稍后重试。”

程序处理

  1. 处理方式与 500 错误完全相同。这些错误是暂时性故障的典型代表,非常适合通过自动重试机制来优雅地处理,从而对用户屏蔽掉后端基础设施的短暂抖动。

503 Service Unavailable

原因:服务器当前无法处理请求。这通常是一个临时状态,可能是由于服务器过载或正在进行计划内的停机维护。

用户提示:“服务暂时不可用,我们正在进行维护,请稍后重试。”

程序处理

  1. 与其他的 5xx 错误类似,首先应尝试自动重试。
  2. 但有一个特殊之处:应检查响应头中是否存在 Retry-After 字段。如果服务器提供了这个头部,它可能包含一个具体的秒数或一个日期,前端应优先遵循这个指示,而不是使用自己的指数退避策略 。这允许后端更精确地控制客户端的重试行为,例如在长时间维护期间。
  3. 如果 Retry-After 不存在,则回退到标准的指数退避重试策略。

第四类:成功响应中的业务逻辑错误

“成功失败”模式

原因:在许多 API 设计中(尤其是非严格 RESTful 的 API),即使操作在业务层面失败了,HTTP 响应的状态码依然是 200 OK。真正的成功或失败状态被封装在 JSON 响应体内部,通过一个自定义的 codestatus 字段来表示 。例如:{ "code": 10045, "message": "余额不足", "data": null }

这种模式对前端错误处理提出了一个独特的挑战。如果拦截器只处理 HTTP 状态码非 2xx 的情况,那么这类业务错误将被遗漏,并错误地进入 .then() 成功回调中,导致后续的业务逻辑出现混乱。

为了构建一个真正统一的错误处理流程,响应拦截器的成功回调 (onFulfilled) 必须也具备处理错误的能力。它不能简单地直接返回响应,而必须承担起“业务错误审查员”的职责。

程序处理

  1. axios.interceptors.response.use(onFulfilled, onRejected)onFulfilled 函数中:
    • 首先,检查响应体 response.data 是否符合预期的业务成功结构。例如,检查 response.data.code 是否等于约定的成功码(如 020000)。
    • 如果 code 表示业务失败,那么 onFulfilled 函数不应该返回 response。相反,它应该创建一个标准化的 AppError 实例,并主动拒绝 Promise,即 return Promise.reject(new AppError(...))
    • 在这个 AppError 实例中,message 应来自 response.data.messagebusinessCode 应来自 response.data.code
    • 只有当 code 表示业务成功时,才 return response

通过这种方式,无论是 HTTP 协议层面的错误(如 404)还是业务逻辑层面的错误(如“余额不足”),对于最终的调用者(例如,apiClient.post(...).catch(error =>...))来说,都表现为 Promise 被拒绝,并且 catch 块中接收到的 error 对象都是我们标准化的 AppError 实例。这实现了对所有类型错误的最终统一处理。通过这种方式,无论是 HTTP 协议层面的错误(如 404)还是业务逻辑层面的错误(如“余额不足”),对于最终的调用者(例如,apiClient.post(...).catch(error =>...))来说,都表现为 Promise 被拒绝,并且 catch 块中接收到的 error 对象都是我们标准化的 AppError 实例。这实现了对所有类型错误的最终统一处理。

用户提示:用户提示应直接来源于响应体中的 message 字段,并可能根据业务场景进行适当的本地化或润色。

示例映射

  • { "code": 10045, "message": "余额不足" } -> 提示:“您的账户余额不足以完成此交易。”
  • { "code": 20010, "message": "无效的优惠券码" } -> 提示:“您输入的优惠券码无效。”

高级韧性与用户体验模式

智能自动重试

自动重试是提升应用在面对暂时性网络或服务器问题时表现的关键机制。然而,一个设计不当的重试策略可能会适得其反,加剧服务器的负载。

何时重试:必须建立一条清晰的规则:仅对暂时性的服务器错误(5xx)和部分网络问题(如超时)进行自动重试。绝不能对客户端错误(4xx,因为请求本身有问题,重试也无法成功)或业务逻辑失败(因为这代表一个确定的业务状态)进行重试。

指数退避与抖动 (Exponential Backoff with Jitter) 策略

  • 必要性:如果所有客户端在请求失败后都立即重试,或者使用固定的延迟时间重试,当服务器从故障中恢复时,会瞬间收到海量的重试请求,形成“惊群效应”(Thundering Herd),可能导致服务器再次崩溃 。
  • 实现:指数退避策略通过在每次连续重试后,以指数级增加等待时间来避免此问题。加入“抖动”(Jitter)——一个小的随机时间量——可以进一步打散重试请求,防止客户端形成同步的重试波次 。

其核心算法可以表示为:retryDelay = (2^attemptNumber * baseDelay) + randomJitter

// 一个实现指数退避重试的 Axios 拦截器适配器
import axios from 'axios';
import { AxiosError, AxiosInstance } from 'axios';

function createRetryInterceptor(axiosInstance: AxiosInstance, retries = 3, baseDelay = 1000) {
  axiosInstance.interceptors.response.use(null, (error: AxiosError) => {
    const { config, response } = error;

    // 如果没有 config 对象或重试配置被禁用,则直接拒绝
    if (!config | config.retries === 0) { return Promise.reject(error); }
    
    // 检查是否为可重试的错误(5xx 或网络错误) 
    const isRetryable =!response | (response.status >= 500 && response.status <= 599); 
    if (!isRetryable) { return Promise.reject(error); }
    // 设置重试次数
    config.retryCount = config.retryCount | 0;
    if (config.retryCount >= retries) { return Promise.reject(error); }
    
    config.retryCount += 1;
    
    // 计算退避延迟
    const backoff = (2 ** (config.retryCount - 1)) * baseDelay;
    const jitter = backoff * 0.2 * Math.random();
    // 添加最多 20% 的抖动
    const delay = backoff + jitter;
    return new Promise((resolve) => {
        setTimeout(() => resolve(axiosInstance(config)), delay);
    });
  });
}

配置:重试机制应该是可配置的,允许全局设置默认的重试次数(通常建议为 3 次)和延迟基数,并允许在单个请求中覆盖这些配置 。

战略性日志与监控

拦截器作为所有 API 通信的必经之路,是实现集中式、标准化日志记录的理想场所。

记录内容:对于每一个被捕获且需要关注的错误(用户主动取消的请求除外),都应该上报一个结构化的日志对象。这个对象应包含丰富的上下文信息,以便快速定位和诊断问题:

  • 错误信息:标准化的 AppError 对象,包含用户友好的消息、HTTP 状态码和业务码。
  • 请求详情:请求的 urlmethodheaders,以及经过脱敏处理的 payload(必须移除密码、身份证号等敏感信息)。
  • 用户上下文:当前登录用户的 userIduserRole 等(如果可用)。
  • 应用上下文:应用版本号 (appVersion)、当前路由 (route)、发生错误的组件名 (componentName) 等。

集成:这些结构化的日志数据可以被发送到专业的第三方错误监控服务,如 Sentry、LogRocket,或公司内部的日志聚合平台 。这些平台不仅能收集错误,还能提供错误聚合、趋势分析、告警通知以及用户行为回放等强大功能,极大地提升了线上问题的排查效率。  

精心设计错误沟通

向用户展示错误信息是用户体验设计中至关重要的一环。好的错误提示能够安抚用户情绪,引导他们解决问题;而坏的错误提示则会增加用户的挫败感和困惑。

以用户为中心的消息传递原则

  • 清晰,而非惊吓:避免使用“500 错误”、“非法参数”等技术术语。使用简单、直接、符合用户日常语言习惯的词汇 。
  • 解释发生了什么:用用户的视角简要说明问题。例如,不说“保存失败”,而说“您的个人资料未能保存”。
  • 提供解决方案或下一步操作:明确告知用户应该怎么做。例如,“...请稍后重试”或“...请检查您的网络连接”。
  • 使用积极、尊重的语气:避免使用命令式或指责性的语言(如“你不能...”)。对于服务器端问题,使用“我们”来承担责任(“我们的服务暂时出现问题...”);对于客户端输入问题,使用中性的表述(“请检查邮箱格式是否正确”) 。

选择正确的 UI 模式

  • 非侵入式提示 :适用于非关键的、在后台执行的操作失败。例如,文档的自动保存失败。Ant Design 的 Alert 组件是这类提示的一个很好的例子 。
  • 内联验证信息 :专用于表单相关的 400 Bad Request 错误。将错误信息直接显示在对应的输入框下方,提供最直接的反馈。
  • 阻塞式模态框 :适用于中断了用户核心流程的严重错误,需要用户明确确认或执行特定操作。例如,“您的登录已过期,请重新登录以继续。”
  • 全页错误状态 :适用于灾难性的失败,导致页面的核心内容无法渲染。例如,一个仪表盘页面的初始数据加载请求完全失败。