先记住这个答案
在服务端定义继承自 Error 的 BusinessError 与 SystemError,并设置各自固定的 name 属性(如 'BusinessError' 和 'SystemError')。业务错误携带具体 code 与可重试标记,系统错误只含通用信息。客户端 catch 时检查 error.name 或 error.code 进行分流:若为业务错误,则展示友好消息并允许用户修改后重试;若为系统错误,则记录日志并跳转通用错误页。注意:跨网络序列化后 Error 原型不保留,因此不能使用 instanceof,只能通过 name 或显式字段判断。
- 用 Error 子类并设置固定 name 标识
- 客户端按 error.name 或 code 分类处理
- 系统错误需隐藏细节并上报记录
错误类型标记与客户端识别机制
在 Server Actions 内部,可预先定义多个继承自 Error 的错误类,每个类在构造函数中设置自己的 name 值,例如 BusinessError 的 name 固定为 'BusinessError',SystemError 固定为 'SystemError'。业务错误还可以携带业务码 code 和 retryable 布尔值,便于客户端决定交互方式;系统错误则只提供通用 message,不暴露堆栈或数据库细节。
当 Server Actions 抛出错误时,Next.js 会将错误对象序列化并传回客户端。由于序列化仅保留可枚举属性与 name、message 等标准字段,prototype 会丢失,因此 error instanceof BusinessError 必然返回 false。客户端必须采用 error.name === 'BusinessError' 或检查 error.code 的方式分类,这种约定式标记被 Next.js 官方实践广泛采用。
// 服务端定义并抛出(伪代码)
// class BusinessError extends Error {
// constructor(code, message) {
// super(message);
// this.name = 'BusinessError';
// this.code = code;
// this.retryable = true;
// }
// }
// 模拟客户端收到的已序列化错误对象
const receivedErrors = [
{ name: 'BusinessError', message: '余额不足', code: 'INSUFFICIENT_BALANCE', retryable: true },
{ name: 'SystemError', message: '数据库连接失败', code: 'INTERNAL' }
];
function classify(err) {
if (err.name === 'BusinessError') {
console.log(`业务可恢复:${err.code},请修改后重试`);
} else if (err.name === 'SystemError') {
console.log(`系统不可恢复:${err.code},已上报并跳转通用页`);
} else {
console.log('未知类型,按系统错误处理');
}
}
receivedErrors.forEach(classify);查看输出与解释
业务可恢复:INSUFFICIENT_BALANCE,请修改后重试
系统不可恢复:INTERNAL,已上报并跳转通用页代码模拟了跨网络后错误对象只有 name/code 等属性,客户端通过 name 或 code 正确分流。
用户提交订单时的错误区分场景
假设用户在线支付创建订单,Server Actions 调用下单 API。当用户余额不足时,服务端抛出 BusinessError,code 为 'INSUFFICIENT_BALANCE',并设置 retryable 标记为 true。此时客户端捕获后弹出可读提示,建议用户充值或更换支付方式,且表单数据和用户在中断处的输入不会丢失,允许用户修改后再次提交。
若下单过程中数据库连接池耗尽抛出系统异常,则服务端抛出 SystemError,客户端 catch 到后自动引导用户到错误页,同时通过上报接口将错误 digest 提交到监控系统。这样既避免向用户展示技术堆栈,也便于研发定位问题,同时不重复提交相同请求造成脏数据。
跨边界序列化导致的分类失效条件
最容易失效的地方是开发者尝试使用 instanceof 判断错误类型,因为 Server Actions 错误经过 Next.js 的序列化管道后,原始 prototype 完全丢失。即便服务端抛出的是自定义 Error 子类,客户端接收到的也只是一个普通对象,其 name 属性虽然保留,但 instanceof 永远返回 false。因此必须约定使用 name 或额外字段,而不是类实例。
另一个风险是 Error 对象中携带不可序列化属性(如函数、Symbol)会在传递时被剥离,导致某些细节丢失。应对措施:在服务端抛出前将必要字段整理为可序列化标量(字符串或数字),并限制 message 长度。如果需要完整的服务端堆栈,应将原始错误记录在服务端日志中,而不是依赖客户端回传。
容易答错的地方
- 用 instanceof 判断错误类型
- 不少文章建议使用
error instanceof BusinessError区分业务错误,但在 Server Actions 中错误被序列化传递,原型链已经丢失,该判断永远为 false。正确做法是基于 error.name 或自定义 code 字段。测试时应模拟实际序列化后的对象。 - 把系统错误的堆栈直接展示给客户端
- 有些实现会把原始 Error.message 直接传给用户,导致数据库结构、内部服务名等泄露。系统错误必须在服务端捕获并替换为通用提示,原始错误记录到服务端监控,客户端只收到一个稳定的 digest 或通用信息。
面试官还会怎么问?
Server Actions 返回错误后,客户端表单状态应如何保留以支持重试?
可以通过 useActionState 管理表单状态,BusinessError 发生时保留用户输入和错误信息,在组件内直接展示;SystemError 时不保留继续操作,跳转到错误边界。重试动作仅对业务错误开放。
如果服务端抛出的不是 Error 对象而是普通对象,客户端如何处理?
只要能按约定区分 name 或 type 字段即可。但建议统一使用 Error 子类并设置 name,因为 Error 对象包含 message 和堆栈便于服务端日志。若抛普通对象,需要自行处理堆栈丢失问题。
系统错误在客户端被捕获后,用什么策略避免用户重复触发相同请求?
客户端可在 catch 中记录已请求标记并使用防重复提交逻辑;同时服务端在接口入口做幂等校验,如携带幂等键。系统错误发生时禁止自动重试,只允许用户手动刷新。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。