21、Flow与错误处理:catch操作符的深度使用、retry与retryWhen、自定义错误处理策略
说实话,Flow 的错误处理这块,我当年刚接触时也踩过不少坑。你想想看,响应式编程里数据像水流一样源源不断,但水流里万一混进了脏东西怎么办?直接断流?还是过滤掉?或者重启水泵?
嗯,这就是我们今天要聊的核心——Flow 的错误处理。我个人习惯把错误处理分成三个层次:捕获、重试、策略化。咱们一个一个来。
一、catch 操作符:错误捕获的正确姿势
先看最基础的。Flow 里如果发生异常,不处理的话整个流就挂了。但用 catch 操作符,你可以在异常发生时做补救。
flow {
emit(1)
emit(2)
throw RuntimeException("出错了")
emit(3) // 这行不会执行
}.catch { e ->
emit(-1) // 用默认值替代
println("捕获到异常: ${e.message}")
}.collect { value ->
println(value)
}
// 输出: 1, 2, -1, 捕获到异常: 出错了
这里有个关键点:catch 只能捕获它上游的异常。什么意思?就是 catch 之前的操作符和 flow builder 里的异常它能抓到,但 catch 之后的就不归它管了。
重要原则:catch 一定要放在错误源的下游,但放在 collect 的上游。如果你把 catch 放在最前面,那它什么都抓不到。
我在项目中遇到过这样的场景:用户登录时,网络请求失败,我不想直接让整个页面白屏。用 catch 发射一个默认的 UI 状态,用户体验就好多了。
二、retry 与 retryWhen:自动重试机制
有些错误是暂时的,比如网络抖动。直接放弃太可惜了。这时候 retry 和 retryWhen 就派上用场了。
2.1 retry:简单粗暴的重试
flow {
emit("请求数据...")
throw IOException("网络连接失败")
}.retry(3) // 最多重试3次
.catch { e -> emit("最终失败: ${e.message}") }
.collect { println(it) }
retry(3) 的意思是:如果上游抛出异常,就重新订阅整个流,最多重试 3 次。如果 3 次都失败,异常会继续往下传,被 catch 捕获。
小技巧:retry 默认会立即重试。如果网络不好,建议配合延迟使用。后面会讲。
2.2 retryWhen:精细化控制重试
retryWhen 给了你更大的控制权。你可以根据异常类型、重试次数、甚至当前时间来决定是否重试。
flow {
emit("开始请求")
throw IOException("超时")
}.retryWhen { cause, attempt ->
// cause: 当前异常
// attempt: 已经重试的次数(从0开始)
if (cause is IOException && attempt < 3) {
delay(1000L * (attempt + 1)) // 递增延迟:1s, 2s, 3s
println("第${attempt + 1}次重试...")
true // 返回true表示继续重试
} else {
println("不满足重试条件,放弃")
false // 返回false表示停止重试
}
}.catch { e ->
println("最终失败: ${e.message}")
}.collect { println(it) }
我曾经用 retryWhen 做过一个指数退避的重试策略。第一次重试等 1 秒,第二次等 2 秒,第三次等 4 秒……这样既不会把服务器打爆,又能提高成功率。
注意:retryWhen 的 lambda 里如果抛出异常,会立即终止重试,并把新异常往下传。所以记得在 lambda 内部做好异常处理。
三、自定义错误处理策略
实际项目中,错误处理往往不是简单的 catch 或 retry 能搞定的。你需要一套完整的策略。我一般会封装一个通用的错误处理函数。
// 自定义错误处理策略
sealed class ErrorStrategy {
data class Retry(val maxRetries: Int, val delayMs: Long) : ErrorStrategy()
data class Fallback(val defaultValue: Any) : ErrorStrategy()
object Propagate : ErrorStrategy() // 直接抛给上层
}
// 扩展函数
fun <T> Flow<T>.handleErrors(
strategy: ErrorStrategy
): Flow<T> = when (strategy) {
is ErrorStrategy.Retry -> this.retryWhen { cause, attempt ->
if (attempt < strategy.maxRetries) {
delay(strategy.delayMs)
true
} else false
}
is ErrorStrategy.Fallback -> this.catch {
emit(strategy.defaultValue as T)
}
is ErrorStrategy.Propagate -> this // 不做处理
}
// 使用示例
fun loadData(): Flow<String> = flow {
emit("数据")
throw RuntimeException("加载失败")
}
fun main() {
loadData()
.handleErrors(ErrorStrategy.Retry(maxRetries = 2, delayMs = 500))
.catch { println("最终还是失败了") }
.collect { println(it) }
}
这样做的好处是,业务代码里只需要调用 handleErrors,传入不同的策略即可。代码干净,逻辑清晰。
四、知识体系总览
下面这张图是我自己总结的 Flow 错误处理全景图。你可以看到,从异常发生到最终处理,有三条路径:捕获、重试、策略化。
五、避坑指南与最佳实践
最后,分享几个我实战中总结的经验:
- catch 不要滥用:不是所有异常都要吞掉。对于不可恢复的错误(比如空指针、参数错误),直接让应用崩溃反而更容易定位问题。
- 重试要有上限:我曾经见过一个同事没设重试上限,结果用户手机在弱网环境下无限重试,电量哗哗地掉。后来加了最大重试次数和延迟,问题就解决了。
- 区分业务异常和技术异常:业务异常(比如用户名不存在)用 catch 发射错误状态;技术异常(比如网络超时)用 retry 重试。混在一起处理,代码会变得很难维护。
- 测试你的错误处理:写单元测试时,专门构造异常场景,看看 catch 和 retry 是否按预期工作。我习惯用
flowOf配合flow { throw ... }来模拟。
核心总结:catch 负责「出错了怎么办」,retry 负责「出错了再来一次」,自定义策略负责「不同错误不同处理」。三者结合,你的 Flow 就能在各种异常场景下稳如泰山。
好了,关于 Flow 的错误处理就聊到这里。记住,好的错误处理不是让程序不出错,而是出错后还能优雅地继续运行。这才是响应式编程的魅力所在。