异常定义
Oak 的异常体系非常重要,因为它不只是为了报错,更是为了让前后端都能理解“这次失败到底属于哪一种业务语义”。
如果你在 Oak 项目里把所有错误都写成普通 Error,短期内也许能跑,但你会很快失去:
- 明确的业务语义;
- 前端可识别的错误类型;
- opRecords 同步能力;
- 统一的提示处理方式。
OakException 是根
oak-domain/src/types/Exception.ts 中,所有 Oak 异常都继承自 OakException。
OakException 除了 message 之外,还会携带:
opRecords_moduleparams
这意味着,一个异常本身也可以带着“为了修正前端缓存而需要同步的数据”一起返回。
最重要的一层: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 内置异常不够用,你当然可以自定义,但建议遵守下面两个原则:
- 尽量继承现有语义最接近的父类;
- 保持异常数据可序列化;
- 用户可见消息使用
error::...locale key,并通过_module与params提供翻译上下文。
例如:
- 输入校验问题,优先继承
OakInputIllegalException; - 权限问题,优先使用
OakOperationUnpermittedException、OakDataInvisibleException等相关权限异常; - 纯系统内部故障,再考虑更底层的
OakException。
makeException 的意义
Exception.ts 最后提供了一个 makeException(...) 方法,用于根据序列化后的数据重新构造异常对象。
这说明 Oak 的异常并不是“只在服务端抛完就算了”,而是被设计成可以跨前后端传播和重建的。也正因为如此,乱抛 Error 会让这套机制失去意义。
一个实践经验
在 Oak 中,异常本身就是业务协议的一部分。
所以写异常时不要只想着“报个错”,而要想清楚:
- 这属于哪一类业务语义;
- 前端是否需要识别它;
- 是否需要带回 opRecords 修正缓存;
- 用户应该看到什么提示。
一旦这样思考,Oak 的异常体系就会变得非常顺手。