很多前端项目都有一个默认前提:
- 浏览器会报错;
- 开发者打开 console 看;
- 真有问题再自己排。
这在小项目里还能勉强工作,但在 Odoo 这种大型 Web Client 里,完全不够。
因为真实世界里的前端错误,不只一种:
- 普通 JS 异常;
- Promise 未处理拒绝;
- RPC 返回的服务端异常;
- 断网导致的连接错误;
- 第三方脚本跨域报错;
- 浏览器扩展乱入;
- 访客用户根本不该看到技术细节。
所以 Odoo 的前端错误系统要解决的,根本不是“控制台有没有红字”,而是:
异常进入前端运行时之后,怎样先分类、再分流、最后决定给谁看什么。
一、error_service 先把浏览器原始异常统一包装
error_service.js 会监听两类核心入口:
window.errorwindow.unhandledrejection
然后把原始错误整理成几类统一对象:
UncaughtClientErrorUncaughtPromiseErrorThirdPartyScriptError- 以及特殊的
HTMLElementLoadingError
这一步很关键。
因为浏览器原生错误来源太杂,字段也不稳定。如果不先做统一包装,后面的处理器几乎没法建立稳定协议。
Odoo 的做法等于先把“浏览器事件”翻译成“框架可理解的错误对象”。
二、真正的主角不是某个 try/catch,而是处理器注册表
handleError() 里最值得学的一段,是它会遍历:
registry.category("error_handlers").getEntries()
也就是说,Odoo 不是把所有错误逻辑硬写死在一个文件里,而是交给一个可扩展的处理器链。
这非常像中间件思路:
- 每个 handler 判断自己能不能处理;
- 能处理就接管;
- 不能处理就交给下一个;
- 全部都不接,再走兜底。
这让错误处理不再是“一个巨大 if/else 墓地”,而是一条可插拔、可排序、可扩展的管线。
三、为什么 cause 要一路往里钻
源码里会不断沿着 error.cause 往下找到最原始的 originalError。
这一步经常被忽略,但非常重要。
因为很多框架级错误,外面包了好几层壳:
- 最外层是 Promise 未处理;
- 里面可能是 RPCError;
- 再里面才是真正业务异常。
如果只看最外层,你只能知道“Promise 爆了”;继续向下找,才知道:
- 是连接断了;
- 是后端抛了业务异常;
- 还是第三方脚本在捣乱。
成熟错误系统,看的从来不是外壳,而是根因。
四、RPC 错误为什么要单独接管
error_handlers.js 里,rpcErrorHandler 优先判断:
- 这是不是
UncaughtPromiseError; - 里面包的是不是
RPCError。
如果是,处理逻辑就明显升级了:
- 先
preventDefault(),避免浏览器按普通未处理拒绝乱报; - 再看
exceptionName或exception_class; - 优先尝试命中
error_notifications或error_dialogs注册表; - 最后才兜底到
RPCErrorDialog。
这套设计特别像后端里的异常映射:
- 某些异常只该给提示气泡;
- 某些异常要专门弹一个业务化对话框;
- 再不行才退回通用错误弹窗。
也就是说,Odoo 没把 RPC 错误当成“普通 JS 爆了”,而是承认:
这本质上是一类产品语义更强的异常。
五、断网不是“报错一下”,而是一条恢复链路
lostConnectionHandler 也很精彩。
当捕获到 ConnectionLostError 时,它不会只弹一句“网络错误”。而是:
- 先弹一个 sticky 通知;
- 再轮询
/web/webclient/version_info; - 使用指数退避 + 随机抖动;
- 恢复后主动提示“你已经重新在线”。
这背后体现的不是报错技巧,而是产品意识。
因为断网对用户来说不是“一个异常对象”,而是一段状态:
- 现在离线;
- 正在重连;
- 已恢复。
Odoo 把它当状态机处理,而不是当一次性报错处理。
六、默认处理器负责兜底,但它并不是第一个出手的人
defaultHandler 的 sequence 是 100,排在更后面。
它做的事情反而很简单:
- 根据错误类型选择
ClientErrorDialog、NetworkErrorDialog或通用ErrorDialog; - 再把 traceback、message、name 等信息交给弹窗。
这恰好说明兜底处理器的角色:
- 它不需要最懂业务;
- 它只要保证“前面都没接住时,系统仍然给出统一且可控的反馈”。
所以稳定系统不怕有兜底,怕的是没有兜底。
七、对访客吞错,不是掩耳盗铃,而是权限边界
很容易被忽略的一段,是 swallowAllVisitorErrors:
- 如果不是内部用户;
- 又不是 debug 或 test 模式;
- 直接返回 true,吞掉错误。
很多人一看到“吞错”就本能反感,但这里恰恰是合理的。
因为公开前台访客看到技术 traceback 往往只有坏处:
- 带来恐慌;
- 暴露实现细节;
- 对解决问题没有帮助;
- 还可能影响品牌感受。
换句话说,错误信息不是越透明越好,而是要按角色分层。
内部用户需要足够信息定位问题;访客用户只需要界面别被技术细节砸脸。
八、Odoo 甚至主动给某些“噪声错误”降噪
比如 ResizeObserver loop completed with undelivered notifications. 这类浏览器噪声,源码会直接 preventDefault() 并忽略。
还有第三方脚本跨域导致的 redacted error、某些浏览器扩展乱扔的异常,也会根据 debug 状态决定是否继续展示。
这点很现实。
因为大型前端系统的目标不是“把所有噪声都喊出来”,而是:
- 真问题别漏;
- 假信号别淹没真正报警。
这和监控系统里的降噪思路完全一致。
九、二开时最该学什么
不是去复制每个类名,而是学这条分层链路:
- 统一入口:浏览器原始错误先标准化;
- 分类对象:客户端、Promise、第三方脚本、RPC 各自归类;
- 处理器注册表:按 sequence 分流;
- 角色分层:内部用户与访客看到不同层次的信息;
- 统一兜底:没人接住时仍然可控。
只要把这几层搭起来,错误系统才算从“开发者工具附件”升级成“产品基础设施”。
十、总结:错误处理真正管理的不是异常,而是信息出口
看完 Odoo 这一套,很容易得出一个结论:
前端异常处理不是在管理 error 对象,而是在管理错误信息怎样流向用户、开发者和系统自身。
有的错误该进通知; 有的该进业务对话框; 有的该静默吞掉; 有的该在 console 保留完整 traceback。
当系统能把这些出口分清,前端报错才真正从“红字”升级成“运行时治理能力”。
DISCUSSION
评论区