Skip to content

异常处理与失败重试

源:Flow exceptions

在响应式编程中,异常处理不仅关乎程序的稳定性,更决定了数据流的恢复能力。Flow 通过一套严谨的契约和丰富的算子,提供了从错误捕获、备选恢复到复杂重试逻辑的完整闭环。

核心契约:异常透明性

异常透明性(Exception Transparency)要求:上游流的实现必须对下游抛出的异常保持透明。

严禁截断下游异常

禁止在 flow { ... } 构建块内部使用 try-catch 包裹 emit 调用。

kotlin
val illegalFlow = flow {
    try {
        emit(data)
    } catch (e: Exception) {
        // ❌ 违规:这会捕获下游 collect 闭包中的异常!
        // 导致下游的取消信号或业务崩溃被上游无意中“消化”。
    }
}

设计原则:上游只负责生产数据并处理自身的副作用;下游产生的错误应由下游自行负责,或者通过结构化并发自然冒泡。

错误捕获与恢复:catch

catch 算子是 Flow 处理异常的标准方式。它具有精确的作用域隔离特性。

catch 的作用域

catch 只能捕获在其位置上方发生的所有异常(包括生产者及中间算子)。它对下方的操作符以及 collect 终端算子抛出的异常无能为力。

kotlin
flow {
    emit(api.fetchData()) 
}
.map { it.process() } // 若此处出错,将被下方 catch 捕获
.catch { e ->
    // ⭐️ 恢复机制:发送备选数据,让流以正常状态继续
    emit(FallbackData)
}
.collect { ... }
kotlin
dataFlow
    .catch { e -> 
        if (e is NetworkException) emit(CacheData) // 处理网络异常
        else throw e // 无法处理则继续向上抛出
    }
    .map { transform(it) }
    .catch { e -> 
        emit(EmptyState) // 捕获变换阶段产生的异常
    }

失败重试:retry 系列算子

当流发生异常时,我们往往希望能够重新触发上游逻辑。Flow 提供了两个核心重试算子。

1. retry:简单计数重试

retry 是最基础的封装,允许你指定重试次数和过滤条件。

kotlin
remoteFlow.retry(retries = 3) { cause ->
    // 只有网络错误才触发重试,返回 true 表示继续重试
    cause is IOException 
}

2. retryWhen:声明式重试逻辑

retryWhen 提供了更高的灵活性。它接收一个 FlowCollector,允许你访问当前的重试次数(attempt)和异常原因(cause)。

  • 控制权:你可以决定是继续重试(返回 true)、停止并报错(返回 false)还是发送一个额外的值。
kotlin
flow { emit(api.call()) }
    .retryWhen { cause, attempt ->
        if (cause is IOException && attempt < 3) {
            delay(1000) // 延迟一秒后再试
            true
        } else {
            false // 超过次数或非网络错误,停止重试
        }
    }

工业级实战:指数退避重试 (Exponential Backoff)

在分布式系统中,为了避免重试导致服务器雪崩,通常会采用指数退避策略:随着重试次数增加,等待时间呈指数级增长。

kotlin
/**
 * 指数退避重试扩展
 * @param maxAttempts 最大重试次数
 * @param initialDelay 初始延迟时间
 * @param factor 增长因子
 */
fun <T> Flow<T>.retryWithBackoff(
    maxAttempts: Int,
    initialDelay: Long = 1000L,
    factor: Double = 2.0
): Flow<T> = retryWhen { cause, attempt ->
    if (cause is IOException && attempt < maxAttempts) {
        // 计算延迟:1s, 2s, 4s, 8s...
        val nextDelay = (initialDelay * Math.pow(factor, attempt.toDouble())).toLong()
        delay(nextDelay)
        true
    } else {
        false
    }
}

异常转换 (Exception Mapping)

有时我们需要将底层库的异常(如 SQLException)转换为领域业务异常(如 RepositoryException)。

kotlin
dbFlow
    .catch { e ->
        // 将底层异常包装后抛出,方便 UI 层处理
        throw RepositoryException("数据库操作失败", e)
    }
    .collect { ... }

总结:完整的容错链条

一个健壮的 Flow 处理链通常包含以下层次:

  1. onStart: 初始化环境。
  2. 业务逻辑: 数据请求与变换。
  3. retryWhen: 处理瞬时错误(如网络波动)。
  4. catch: 处理持久错误(如权限不足),提供降级方案(发射默认值)。
  5. onCompletion: 无论成败,执行最终清理。

核心开发准则

  1. 不要在中间算子吞掉异常:除非你确定该异常是良性的且有明确的补救数据。
  2. 区分重试类型:对于 404 等逻辑错误,不应重试;仅针对 5xx 或网络超时执行重试逻辑。
  3. 合理利用 try-catch 保护 collectcatch 算子无法保护 collect 块。如果 UI 渲染逻辑可能崩溃,必须在 collect 外层包裹标准的 try-catch
  4. 利用 onCompletion 进行调试:在该钩子中记录流的结束状态(成功、被取消或崩溃的具体堆栈),是生产环境排查问题的利器。