Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

异常定义

Oak 的异常体系非常重要,因为它不只是为了报错,更是为了让前后端都能理解“这次失败到底属于哪一种业务语义”。

如果你在 Oak 项目里把所有错误都写成普通 Error,短期内也许能跑,但你会很快失去:

  • 明确的业务语义;
  • 前端可识别的错误类型;
  • opRecords 同步能力;
  • 统一的提示处理方式。

OakException 是根

oak-domain/src/types/Exception.ts 中,所有 Oak 异常都继承自 OakException

OakException 除了 message 之外,还会携带:

  • opRecords
  • _module
  • params

这意味着,一个异常本身也可以带着“为了修正前端缓存而需要同步的数据”一起返回。

最重要的一层:OakUserException

从实际开发角度看,最重要的父类其实是 OakUserException

它表示:

这是一个可预期的、由用户操作引发的异常。

只要你的错误属于“用户做了不被允许的事、或者输入的数据不合法”,通常都应该优先继承 OakUserException 或它的子类,而不是直接抛普通 Error。

常见异常类型

下面这些异常,是 Oak 项目里最常见的一批:

异常含义
OakRowInconsistencyException当前数据状态不允许这次操作
OakInputIllegalException输入非法
OakAttrNotNullException非空属性为空
OakAttrCantUpdateException某属性不允许被更新
OakOperationUnpermittedException当前用户无权执行此操作
OakDataInvisibleException当前用户无权看到这批数据
OakUnloggedInException用户未登录
OakPreConditionUnsetException某个前置条件未满足
OakExternalException调用外部接口失败
OakUniqueViolationException唯一约束冲突
OakImportDataParseException导入数据解析失败
OakApplicationHasToUpgrade应用需要升级

为什么这些异常有意义

例如下面这几个异常,虽然看起来都像“失败了”,但语义完全不同:

  • OakInputIllegalException:是你传进来的参数错了;
  • OakOperationUnpermittedException:是你没权限做这件事;
  • OakDataInvisibleException:是你连看这批数据都不该看;
  • OakExternalException:是第三方系统失败了,不一定是你输入错。

当前端能区分这些语义时,提示方式、恢复方式、重试方式都会完全不同。

一个典型写法

业务异常应优先使用可翻译的 key、模块名和结构化参数:

if (!license) {
    throw new OakUserException(
        'error::license.notFound',
        'project-name',
        { licenseId: params.licenseId }
    );
}

而在 checker 中,更推荐使用更具体的异常,例如:

throw new OakAttrNotNullException('address', ['name'], '地址命名不能为空');

自定义异常时的建议

如果 Oak 内置异常不够用,你当然可以自定义,但建议遵守下面两个原则:

  1. 尽量继承现有语义最接近的父类;
  2. 保持异常数据可序列化;
  3. 用户可见消息使用 error::... locale key,并通过 _moduleparams 提供翻译上下文。

例如:

  • 输入校验问题,优先继承 OakInputIllegalException
  • 权限问题,优先使用 OakOperationUnpermittedExceptionOakDataInvisibleException 等相关权限异常;
  • 纯系统内部故障,再考虑更底层的 OakException

makeException 的意义

Exception.ts 最后提供了一个 makeException(...) 方法,用于根据序列化后的数据重新构造异常对象。

这说明 Oak 的异常并不是“只在服务端抛完就算了”,而是被设计成可以跨前后端传播和重建的。也正因为如此,乱抛 Error 会让这套机制失去意义。

一个实践经验

在 Oak 中,异常本身就是业务协议的一部分。

所以写异常时不要只想着“报个错”,而要想清楚:

  • 这属于哪一类业务语义;
  • 前端是否需要识别它;
  • 是否需要带回 opRecords 修正缓存;
  • 用户应该看到什么提示。

一旦这样思考,Oak 的异常体系就会变得非常顺手。