Kotlin Coroutine 异常处理机制全面解析
Kotlin Coroutine 异常处理机制全面解析
引言
Kotlin Coroutines 已经成为 Android 和 Kotlin 后端开发中处理异步操作的标准方案。然而,异常处理机制是协程中最容易让人困惑的部分之一——launch 和 async 的异常传播行为不同,SupervisorJob 和普通 Job 的表现也不同,结构化并发的取消和异常传播更是让很多开发者踩坑。
本文从协程异常处理的底层原理出发,覆盖所有常见场景和最佳实践,帮助你彻底理解协程的异常处理机制。
协程异常处理基础
在传统编程中,try-catch 是异常处理的唯一方式。但在协程的世界里,异常处理的方式取决于你使用的是 launch 还是 async,以及协程的层次结构。
try-catch 在协程中的局限性
考虑以下代码:
fun main() = runBlocking {
try {
launch {
throw RuntimeException("协程异常")
}
} catch (e: Exception) {
println("捕获到异常: $e")
}
}
这段代码能捕获异常吗?不能。 抛出异常的协程在 launch 内部,而 try-catch 包裹的是 launch 本身(它是非阻塞的)。异常在协程体内部抛出时,launch 已经返回了。
正确的方式是将 try-catch 放在协程体内部:
fun main() = runBlocking {
launch {
try {
throw RuntimeException("协程异常")
} catch (e: Exception) {
println("捕获到异常: $e")
}
}
}
这是最简单的异常处理方式,但在复杂场景下不够用。
Launch 与 Async 的异常传播差异
这是协程异常处理的核心知识点。launch 和 async 对异常的处理方式完全不同。
Launch:主动传播异常
launch 创建的协程遇到未捕获异常时,会立即将异常传播给父协程,并取消父协程及其所有子协程。
fun main() = runBlocking {
val scope = CoroutineScope(Job())
scope.launch {
println("子协程 1 开始")
delay(100)
throw RuntimeException("子协程 1 异常")
}
scope.launch {
println("子协程 2 开始")
try {
delay(500)
} catch (e: CancellationException) {
println("子协程 2 被取消了")
}
}
delay(1000)
println("主协程结束")
}
// 输出:
// 子协程 1 开始
// 子协程 2 开始
// 子协程 2 被取消了
// 子协程 1 异常 -> 未捕获异常,程序崩溃
因为两个子协程共享同一个父 Job,子协程 1 的异常会传播给父 Job,父 Job 取消自己并取消所有子协程。
Async:等待时才暴露异常
async 将异常延迟到调用 .await() 时才抛出。这与 Future.get() 的行为类似——异常被存储起来,等待获取结果时才暴露。
fun main() = runBlocking {
val deferred = async {
throw RuntimeException("async 异常")
}
delay(100) // async 已经执行完毕,异常被存储
println("这里还能执行")
try {
deferred.await() // 异常在这里抛出
} catch (e: Exception) {
println("在 await() 时捕获: $e")
}
}
这个特性非常关键:如果 async 的结果被忽略(从不调用 await()),异常也不会被抛出——但协程框架仍然会记���它,并在未处理时触发全局异常处理器。
fun main() = runBlocking {
val scope = CoroutineScope(Job())
val deferred = scope.async {
throw RuntimeException("被忽略的异常")
}
// 从不调用 deferred.await()
delay(500)
// 程序退出时,异常由 CoroutineExceptionHandler 处理
}
这会导致异常在协程被垃圾回收时由未捕获异常处理器处理,通常意味着程序崩溃。
核心对比
| 特性 | launch | async |
|---|---|---|
| 异常传播时机 | 立即传播 | await() 调用时传播 |
| 向上传播 | 立即传递给父协程 | 等待 await() |
| try-catch 位置 | 协程体内部 | 包裹 await() |
| 未处理后果 | 全局异常处理器 | 全局异常处理器(仅在 await 被调用时) |
结构化并发下的异常传播
结构化并发是协程的核心设计原则:子协程中的异常会取消父协程,并级联取消所有兄弟协程。
默认行为:异常会向上取消整个作用域
fun main() = runBlocking {
val job = launch {
launch {
delay(100)
throw RuntimeException("子协程 A 异常")
}
launch {
delay(200)
println("子协程 B 完成")
}
}
job.join()
println("父协程完成")
}
// 输出:
// 子协程 A 异常 -> 子协程 B 被取消
// 父协程被取消
这个模型保证了:任何一个子协程失败,整个作用域内的所有协程都停止工作。 这在很多场景下是合理的——如果一个子任务失败了,整体结果也就无意义了。
捕获父协程的异常
既然异常会传播到父协程,我们可以通过捕获父协程的异常来统一处理:
fun main() = runBlocking {
val job = launch {
try {
launch {
delay(100)
throw RuntimeException("子协程异常")
}
} catch (e: Exception) {
// 这能捕获到吗?
println("在父协程中捕获: $e")
}
}
}
不能。 因为 try-catch 包裹的是 launch 调用本身(也是非阻塞的),而不是子协程的执行。
正确的方式是在父协程体中使用 try-catch 包裹子协程的调用:
fun main() = runBlocking {
launch {
launch {
try {
delay(100)
throw RuntimeException("子协程异常")
} catch (e: Exception) {
println("在子协程中捕获: $e")
}
}
}
}
或者,使用 SupervisorJob 阻止异常向上传播。
SupervisorJob:隔离异常传播
SupervisorJob 是解决子协程异常影响兄弟协程的关键工具。
基本用法
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
delay(100)
throw RuntimeException("子协程 1 异常")
}
scope.launch {
delay(200)
println("子协程 2 完成")
}
delay(500)
println("作用域完成")
}
// 输出:
// Exception in thread "..." RuntimeException: 子协程 1 异常
// 子协程 2 完成
// 作用域完成
可以看到,子协程 1 的异常没有影响子协程 2 的执行,也没有取消作用域。
SupervisorJob 的工作原理
普通 Job 和 SupervisorJob 的区别在于如何处理子协程的异常:
// Job 的行为:子协程异常 -> 通知父 Job -> 父 Job 取消所有子协程
// fun Job(): Job = JobImpl(true) // true 表示会处理子协程异常
// SupervisorJob 的行为:子协程异常 -> 忽略,不通知父 Job
// fun SupervisorJob(): Job = JobImpl(false) // false 表示不处理子协程异常
关键区别就在 JobImpl 的 handleChildCompletion 参数。普通 Job 在子协程失败时调用 childCancelled() 方法,而 SupervisorJob 不调用。
supervisorScope
supervisorScope 提供了在现有作用域内容器化使用 SupervisorJob 的能力:
fun main() = runBlocking {
supervisorScope {
launch {
delay(100)
throw RuntimeException("子协程异常")
}
launch {
delay(200)
println("兄弟协程正常运行")
}
}
println("supervisorScope 结束")
}
// 输出:
// 兄弟协程正常运行
// supervisorScope 结束
supervisorScope 的典型用途:多个不相关的并发任务,其中一个失败不应该影响其他的。
实际场景:批量网络请求
suspend fun fetchUserData(userIds: List<String>): List<UserData?> {
return supervisorScope {
userIds.map { userId ->
async {
try {
api.fetchUser(userId)
} catch (e: Exception) {
Log.e("TAG", "获取用户 $userId 失败", e)
null // 单个失败不影响其他的
}
}
}.awaitAll()
}
}
这里使用 supervisorScope 确保某个用户的数据请求失败不会取消其他请求。
CoroutineExceptionHandler
CoroutineExceptionHandler 是协程的全局异常处理器,用于处理未捕获的异常。
基本用法
val handler = CoroutineExceptionHandler { _, exception ->
println("全局异常处理器捕获: $exception")
}
fun main() = runBlocking {
val scope = CoroutineScope(Job() + handler)
scope.launch {
throw RuntimeException("未捕获异常")
}
delay(500)
}
// 输出:
// 全局异常处理器捕获: RuntimeException: 未捕获异常
处理器的生效条件
CoroutineExceptionHandler 只在以下情况下生效:
- 异常是未捕获的(没有被
try-catch捕获) - 协程是
launch启动的(async的异常由await()抛出) - 异常传播到了顶级协程(没有父协程可以传递)
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("处理器被调用: $exception")
}
launch(handler) {
launch {
throw RuntimeException("子协程异常")
}
}
}
// 输出:(不会打印处理器消息)
// 子协程异常传播到了父 launch,但父 launch 有父协程(runBlocking)
// 异常最终由 runBlocking 处理
只有在顶级协程(直接由 CoroutineScope 创建的协程)中的异常才会由 CoroutineExceptionHandler 处理:
fun main() {
val handler = CoroutineExceptionHandler { _, exception ->
println("处理器被调用: $exception")
}
val scope = CoroutineScope(Job() + handler)
scope.launch {
launch {
throw RuntimeException("子协程异常")
}
}
Thread.sleep(500)
}
// 输出:
// 处理器被调用: RuntimeException: 子协程异常
// 因为 scope.launch 是顶级协程,异常会传播到这里,由 handler 处理
Handler 与 SupervisorJob 搭配使用
当 SupervisorJob 阻止了异常向上传播时,CoroutineExceptionHandler 也帮不上忙了:
fun main() {
val handler = CoroutineExceptionHandler { _, exception ->
println("处理器: $exception")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
throw RuntimeException("异常")
}
Thread.sleep(500)
}
// 输出:
// 处理器: RuntimeException: 异常
// ✅ SupervisorJob 没有取消父协程,异常由子协程的 handler 处理
设置全局默认处理器
可以通过设置 JVM 级全局处理器来兜底:
// 全局未捕获异常处理器
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
Log.e("APP", "全局未捕获异常: $exception", exception)
}
// 协程框架也会使用这个处理器
或者在 Android 中设置 CoroutineExceptionHandler 作为全局默认处理器(较新版本的 Kotlin 协程支持):
// 在 Application 类中设置
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// Kotlin 协程提供的全局异常处理器
CoroutineExceptionHandler { _, throwable ->
Log.e("APP", "未处理的协程异常", throwable)
// 上报到 Crash 统计
}
}
}
Async 的异常处理:应该在哪里 try-catch
async 的异常处理有两种方式,效果不同。
在 async 内部捕获
fun main() = runBlocking {
val deferred = async {
try {
riskyOperation()
} catch (e: Exception) {
null // 返回默��值
}
}
val result = deferred.await() // 不会抛出异常
}
在 await() 时捕获
fun main() = runBlocking {
val deferred = async {
riskyOperation() // 异常存储在这里
}
try {
val result = deferred.await() // 异常在这里抛出
} catch (e: Exception) {
println("捕获 async 异常: $e")
// 降级处理
}
}
推荐的 Async 异常处理模式
sealed class AsyncResult<out T> {
data class Success<T>(val data: T) : AsyncResult<T>()
data class Error(val exception: Throwable) : AsyncResult<Nothing>()
}
suspend fun <T> CoroutineScope.safeAsync(
block: suspend CoroutineScope.() -> T
): AsyncResult<T> {
return try {
AsyncResult.Success(async { block() }.await())
} catch (e: Exception) {
AsyncResult.Error(e)
}
}
// 使用
fun main() = runBlocking {
val result = safeAsync {
fetchUserData()
}
when (result) {
is AsyncResult.Success -> showData(result.data)
is AsyncResult.Error -> showError(result.exception)
}
}
取消引发的 CancellationException
CancellationException 是协程框架中的一个特殊异常,它的处理方式和普通异常不同。
特殊性
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
// 即使协程被取消,finally 块也会执行
println("清理资源")
}
}
delay(100)
job.cancel()
job.join()
}
在 finally 块中调用挂起函数
当协程被取消后,在 finally 块中调用挂起函数需要特殊处理:
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
// ❌ 协程已取消,这里调用挂起函数会再次抛出 CancellationException
delay(100) // 这里会抛出 CancellationException
println("清理完成")
}
}
delay(100)
job.cancel()
job.join()
}
正确的做法是使用 withContext(NonCancellable):
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
// ✅ 使用 NonCancellable 上下文执行挂起函数
withContext(NonCancellable) {
delay(100)
println("资源清理完成")
}
}
}
delay(100)
job.cancel()
job.join()
}
CancellationException 不会被传播
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("处理器不会收到 CancellationException: $exception")
}
val scope = CoroutineScope(Job() + handler)
val job = scope.launch {
launch {
delay(1000)
}
launch {
delay(500)
}
}
delay(100)
job.cancel() // 取消会传递 CancellationException,但不会被异常处理器捕获
job.join()
}
CancellationException 不会被任何异常处理器捕获,也不会导致程序崩溃。
异常聚合
当一个父协程有多个子协程同时失败时,异常会被聚合到一个 CancellationException 中。
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("聚合异常: $exception")
exception.suppressed.forEach {
println(" 包含: $it")
}
}
val scope = CoroutineScope(Job() + handler)
val job = scope.launch {
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw RuntimeException("清理异常 1")
}
}
launch {
try {
delay(Long.MAX_VALUE)
} finally {
throw RuntimeException("清理异常 2")
}
}
}
delay(100)
job.cancelAndJoin()
}
// 输出:
// 聚合异常: kotlinx.coroutines.JobCancellationException: Job was cancelled
// 包含: RuntimeException: 清理异常 1
// 包含: RuntimeException: 清理异常 2
异常聚合通过 Kotlin 的 Throwable.addSuppressed() 机制实现,可以用 exception.suppressed 访问所有被抑制的异常。
Android 中的协程异常处理最佳实践
在 Android 开发中,协程异常处理需要特别注意 UI 层和业务层的分离。
ViewModel 中的异常处理
class UserViewModel(
private val userRepository: UserRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<User>>> = _uiState.asStateFlow()
fun loadUsers() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val users = userRepository.fetchUsers()
_uiState.value = UiState.Success(users)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
Log.e("UserViewModel", "加载用户失败", e)
}
}
}
}
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
Repository 层的异常处理
class UserRepository(
private val api: UserApi,
private val cache: UserCache
) {
suspend fun fetchUsers(): List<User> = withContext(Dispatchers.IO) {
try {
val response = api.getUsers()
cache.saveUsers(response)
response
} catch (e: IOException) {
// 网络错误,尝试从缓存读取
val cached = cache.getUsers()
if (cached.isNotEmpty()) {
cached
} else {
throw e // 缓存也没有,继续抛出
}
} catch (e: HttpException) {
// HTTP 错误,返回缓存的过期数据(如果存在)
cache.getUsers().also { users ->
if (users.isEmpty()) {
throw e
}
}
}
}
}
使用 Result 类型
Kotlin 标准库提供了 Result 类型,适合在 Repository 层包装可能失败的操作:
class UserRepository(private val api: UserApi) {
suspend fun fetchUsers(): Result<List<User>> = runCatching {
api.getUsers()
}
suspend fun fetchUser(id: String): Result<User> = runCatching {
api.getUser(id)
}
}
// ViewModel 中使用
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
fun loadUsers() {
viewModelScope.launch {
repository.fetchUsers()
.onSuccess { users ->
_uiState.value = UiState.Success(users)
}
.onFailure { exception ->
_uiState.value = UiState.Error(exception.message ?: "加载失败")
}
}
}
}
Flow 中的异常处理
class UserRepository(private val api: UserApi) {
fun usersFlow(): Flow<List<User>> = flow {
while (true) {
val users = api.getUsers()
emit(users)
delay(30_000) // 每 30 秒��询
}
}.retry(3) { cause ->
// 网络错误时重试,最多 3 次
cause is IOException
}.catch { exception ->
// 所有重试都失败后,发射空列表
emit(emptyList())
Log.e("UserRepository", "获取用户列表失败", exception)
}
}
常见陷阱与最佳实践
陷阱 1:错误的 try-catch 位置
// ❌ 错误:try-catch 包裹的是 launch,不是协程体
try {
launch { throw Exception() }
} catch (e: Exception) {
// 永远不会执行
}
// ✅ 正确:try-catch 在协程体内部
launch {
try {
throw Exception()
} catch (e: Exception) {
// 在这里处理
}
}
陷阱 2:忽略 async 的异常
// ❌ 错误:async 的异常被忽略
scope.async {
throw RuntimeException("异常")
}
// ✅ 正确:必须 await()
scope.async {
throw RuntimeException("异常")
}.let { deferred ->
try {
deferred.await()
} catch (e: Exception) {
// 处理异常
}
}
// ✅ 或者如果不需要结果,使用 launch 代替 async
scope.launch {
throw RuntimeException("异常")
}
陷阱 3:在 CancellationException 中调用挂起函数
// ❌ 错误:在 finally 块中调用挂起函数
job = launch {
try {
delay(1000)
} finally {
delay(100) // 协程取消后,这里抛出 CancellationException
cleanup()
}
}
// ✅ 正确:使用 withContext(NonCancellable)
job = launch {
try {
delay(1000)
} finally {
withContext(NonCancellable) {
delay(100)
cleanup()
}
}
}
陷阱 4:在 CoroutineScope 中使用 try-catch 捕获子协程异常
// ❌ 错误:try-catch 不能跨协程边界
scope.launch {
try {
launch { throw Exception() } // 异常发生在这里
} catch (e: Exception) {
// 捕获不到
}
}
// ✅ 正确:在子协程内部捕获
scope.launch {
launch {
try {
throw Exception()
} catch (e: Exception) {
// 在这里处理
}
}
}
陷阱 5:ViewModelScope 中使用 async 不 await
在 Android 的 ViewModel 中,viewModelScope 使用 SupervisorJob,这意味着子协程的异常不会相互影响。但如果你使用 async 而不 await():
class MyViewModel : ViewModel() {
fun load() {
viewModelScope.async { // ❌ 不 await 的 async
throw RuntimeException("异常")
}
// 异常不会崩溃,但会丢失
}
}
虽然不会崩溃,但异常信息会丢失。要么使用 launch,要么确保 await()。
实战:构建健壮的协程异常处理框架
统一错误处理基类
abstract class BaseRepository {
protected suspend fun <T> safeApiCall(
call: suspend () -> T,
errorMessage: String = "请求失败"
): Result<T> = runCatching {
call()
}.recover { exception ->
val wrappedException = when (exception) {
is IOException -> NetworkException(errorMessage, exception)
is HttpException -> HttpException(exception.code(), errorMessage)
else -> UnknownException(errorMessage, exception)
}
throw wrappedException
}
protected suspend fun <T> safeDbCall(
call: suspend () -> T
): Result<T> = runCatching {
call()
}.recover { exception ->
throw DatabaseException("数据库操作失败", exception)
}
}
全局异常监控
// 在 Application 中初始化
class MyApplication : Application() {
lateinit var coroutineExceptionHandler: CoroutineExceptionHandler
private set
override fun onCreate() {
super.onCreate()
coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
when (throwable) {
is NetworkException -> {
Log.w("APP", "网络异常: ${throwable.message}")
}
is CancellationException -> {
// 忽略正常取消
}
else -> {
Log.e("APP", "未处理的协程异常", throwable)
// 上报到 Crashlytics 等分析平台
Crashlytics.logException(throwable)
}
}
}
// 设置全局默认处理器
Thread.setDefaultUncaughtExceptionHandler { thread, exception ->
Log.e("APP", "全局未捕获异常", exception)
Crashlytics.logException(exception)
}
}
}
完整的 ViewModel 异常处理模板
abstract class BaseViewModel : ViewModel() {
protected fun <T> launchCatching(
block: suspend CoroutineScope.() -> T,
onError: ((Throwable) -> Unit)? = null
): Job {
return viewModelScope.launch {
try {
block()
} catch (e: CancellationException) {
// 正常取消,不处理
throw e
} catch (e: Exception) {
Log.e(TAG, "协程执行异常", e)
onError?.invoke(e) ?: handleDefaultError(e)
}
}
}
private fun handleDefaultError(exception: Exception) {
val message = when (exception) {
is NetworkException -> "网络连接失败,请检查网络"
is TimeoutException -> "请求超时,请稍后重试"
else -> "操作失败: ${exception.message}"
}
// 通过 SharedFlow 发送给 UI 层
_errorEvent.emit(ErrorEvent(message))
}
private val _errorEvent = MutableSharedFlow<ErrorEvent>()
val errorEvent: SharedFlow<ErrorEvent> = _errorEvent.asSharedFlow()
data class ErrorEvent(val message: String)
}
// 使用
class ProfileViewModel : BaseViewModel() {
fun loadProfile(userId: String) {
launchCatching(
block = {
val profile = repository.getProfile(userId)
_profile.value = profile
},
onError = { exception ->
// 自定义错误处理
_profile.value = Profile.default()
}
)
}
}
总结
Kotlin Coroutine 的异常处理需要理解三个核心概念:
- 传播机制:
launch立即传播异常,async延迟到await()时传播 - 结构化并发:默认子协程异常会取消父协程和兄弟协程,
SupervisorJob可以隔离异常 - 异常处理工具:
try-catch处理'局部'异常,CoroutineExceptionHandler处理'全局'未捕获异常
关键原则:
- 尽量在协程体内部捕获异常,而不是外部
- 兄弟任务互不依赖时使用 supervisorScope
- async 必须搭配 await() 否则异常丢失
- CancellationException 只应在 finally 块中特殊处理
- 在 Android 中使用 viewModelScope + launch + try-catch 处理 UI 层异常
掌握这些机制,你就能写出健壮、可维护的协程代码,再也不会被协程的异常处理问题困扰。