为何Kotlin Arrow的bind仅在显式catch时抛出CancellationException?
现象回顾
在Arrow 1.2.3的either作用域内,直接调用bind()处理Left值时不会抛出可见异常,either会正常返回对应的Left结果;但用原生try/catch包裹bind()调用时,会捕获到如下异常:
Got exception arrow.core.raise.NoTrace: kotlin.coroutines.cancellation.CancellationException should never get swallowed. Always re-throw it if captured.This swallows the exception of Arrow's Raise, and leads to unexpected behavior.When working with Arrow prefer Either.catch or arrow.core.raise.catch to automatically rethrow CancellationException.
改用Arrow内置的catch函数则能正常执行,不会触发该异常。
核心原因
Arrow的bind基于内部异常实现控制流
Arrow的either作用域和bind()函数依赖内部异常实现非局部返回:当bind()处理Left值时,会抛出一个Arrow内部的异常来中断当前代码流程,随后either作用域会捕获这个异常,并将其转换为Left类型返回。这个异常是Arrow控制流的一部分,并非业务异常,也不应该被用户的原生try/catch捕获。原生try/catch破坏了Arrow的控制流
当你用原生try/catch捕获了这个内部异常,either作用域就无法感知到bind()的Left分支,也就无法完成正常的流程转换。此时Arrow会抛出NoTrace异常来警告你:你吞掉了它的控制流异常,这会导致后续逻辑出现意外行为。CancellationException提示的误导性
提示中提到的CancellationException和协程无关,这是因为Arrow的异常处理逻辑复用了协程环境中的安全规则——吞掉CancellationException会导致协程无法正常取消,而这里的场景是你吞掉了Arrow的控制流异常,Arrow用同样的警告逻辑来提醒你不要破坏它的内部流程,所以出现了这个看似不相关的提示。
为什么Arrow的catch函数可以正常工作?
Arrow内置的catch函数是专门为其控制流机制设计的,它会自动区分业务异常和Arrow内部控制流异常:
- 对于业务代码抛出的异常,会交给你定义的处理逻辑处理
- 对于
bind()抛出的内部控制流异常,会重新抛给either作用域处理,保证原有的流程转换正常进行
总结
Arrow的bind()和either作用域的控制流依赖内部异常实现,原生try/catch会干扰这个机制,导致Arrow抛出警告异常。必须使用Arrow提供的catch函数来处理异常,才能和它的控制流正确协作。
内容的提问来源于stack exchange,提问作者John Schlachtenhaufen

