Kotlin 协程异常的黄金准则

1,395 阅读11分钟

0.jpg

之前我写过一篇文章 Kotlin协程异常一文通,那篇文章直接从异常的函数入手,讲述了 Kotlin 协程的特点和部分用法。

我把那篇文章分享给了同事,过了一段时间,他还是来问我:coroutineScope 处理异常有什么特点?

虽然那篇文章我认为写的非常明确,但是我想他既然记不住,可能太教科书了。我告诉他,你等我下篇文章,我给你讲个明明白白。

于是,我打算从案例入手,来几条黄金准则,一篇文章搞懂在 Kotlin 协程中如何处理异常。

协程异常处理的核心机制是结构化并发

这是 Kotlin 协程的设计原则,也是为什么协程异常有点难处理的原因。

你可以把它想象成一棵任务树:只要某个子协程因异常失败,就会立即通知父协程;父协程会马上取消所有其他子协程,随后自身也被取消,并将异常继续向上传播。

这种机制能确保不会有任何“孤儿协程”被遗漏,让协程的生命周期始终处于可控状态。

下面我们结合实际开发场景,拆解协程异常处理的核心逻辑和正确姿势。

场景一:使用 launch

launch 是典型的 “fire-and-forget(发起即忘)”型协程构建器,适合不需要返回结果的任务。但它的异常处理逻辑很容易踩坑,新手常在这里栽跟头。

错误做法:在 launch 外层套 try-catch

这是高频错误用法:把 launch 调用包裹在 try-catch 里,这样做根本无法捕获协程内部抛出的异常

问题原因

launch 会立即返回,而协程体里的代码是在后台线程异步执行的。等异常真正抛出时,外层的 try-catch 早就执行完了。最终异常会一路冒泡到顶层异常处理器,若无人处理,应用就会直接崩溃。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Before launch")
    try {
        // 这个 launch 块会抛出异常
        launch {
            println("Inside launch: Throwing exception...")
            delay(500) // 模拟耗时操作
            throw RuntimeException("Something went wrong!")
        }
    } catch (e: Exception) {
        // ❌ 这段代码永远不会执行 ❌
        println("Caught exception: $e")
    }
    println("After launch")
    delay(1000) // 保持主线程存活,观察崩溃现象
    println("Main finished")
}
Before launch
After launch
Inside launch: Throwing exception...
Exception in thread "main" java.lang.RuntimeException: Something went wrong!
...

可以看到,“Caught exception” 从未打印,程序直接崩溃。

正确做法:在 launch 内部使用 try-catch

要处理某个 launch 协程的异常,必须把 try-catch 直接放在协程的 lambda 表达式内部。

这个做法简直是异常处理的古法捕获!

这样能让异常处理逻辑紧跟异常发生的位置——也就是协程自身的执行路径上。只有这样,才能优雅处理错误,避免整个作用域崩溃。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Before launch")
    launch {
        try {
            // 风险代码放在 try 块中
            println("Inside launch: Doing some work...")
            delay(500)
            throw RuntimeException("Oops, failed!")
        } catch (e: Exception) {
            // ✅ 正确捕获异常
            println("Caught exception inside launch: ${e.message}")
        }
    }
    println("After launch")
    delay(1000) // 等待 launch 执行完成
    println("Main finished")
}

Before launch
After launch
Inside launch: Doing some work...
Caught exception inside launch: Oops, failed!
Main finished

此时程序能正常处理异常,不会崩溃。

场景二:使用 async 构建器

当需要执行任务并后续获取结果时,用 async 构建器,它会返回一个 Deferred 对象。async 的异常处理逻辑和 launch 不同——它会“暂存”异常,直到你调用 await() 获取结果时才会抛出。

错误做法:在 async 调用周围套 try-catch

仅把 async 本身包裹在 try-catch 里是无效的。

问题原因

async 会立即返回 Deferred 对象,协程内部的异常会被存储在这个对象中,等待你显式调用 await() 时才会暴露。因此,在 async 外层 try-catch 根本捕获不到这个延迟出现的异常。

正确做法:在 await() 调用周围用 try-catch

只有调用 await() 获取结果时,async 内部的异常才会被重新抛出——这是异常真正“显现”的时刻。

这种设计让你可以自主决定处理失败的时机和方式。异常是“延迟结果”的一部分,只有主动请求结果时才会触发异常。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Before async")
    val deferredResult: Deferred<String> = async {
        println("Inside async: About to fail...")
        delay(500)
        throw IllegalStateException("Async operation failed!")
        "This will never be returned"
    }
    println("After async")
    try {
        // 异常在调用 await() 时抛出
        val result = deferredResult.await()
        println("Result: $result")
    } catch (e: Exception) {
        // ✅ 正确捕获异常
        println("Caught exception on await: ${e.message}")
    }
    println("Main finished")
}

Before async
After async
Inside async: About to fail...
Caught exception on await: Async operation failed!
Main finished
Exception in thread "main" java.lang.IllegalStateException: Async operation failed!
... (堆栈信息) ...

注意:虽然 catch 块成功捕获了异常,但由于顶层异常未处理,程序仍会崩溃。

这里正好体现了协程异常向上传播的特性。

若要完全避免崩溃,需在顶层添加异常处理逻辑,或使用 CoroutineScope 的异常处理器。

或者,你继续往下看!

替代方案:在 async 内部使用 try-catch

async 内部处理异常,能实现错误本地化,并可根据处理逻辑从 catch 块返回结果。这种情况下,Deferred 对象返回后不会再抛出异常——因为异常已经提前处理完毕。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Before async")
    val deferredResult = async {
        try {
            println("Inside async: About to fail...")
            delay(500)
            throw IllegalStateException("Async operation failed!")
        } catch (e: Exception) {
            println("Caught exception inside async: ${e.message}")
            return@async "Fallback result" // 异常处理后返回默认结果
        }
        "This will never be returned"
    }
    println("After async")
    val result = deferredResult.await()
    println("Result: $result")
    println("Main finished")
}

Before async
After async
Inside async: About to fail...
Caught exception inside async: Async operation failed!
Result: Fallback result
Main finished

此时程序不会崩溃,因为异常已在 async 内部处理完毕,await() 能正常获取到 catch 块返回的结果。

场景三:父子协程关系

这是结构化并发的核心价值所在。coroutineScope 会等待所有子协程完成,一旦任意一个子协程失败,整个作用域会立即取消其他所有子协程,随后自身也失败。

一个子协程失败,全作用域取消

这种机制能防止程序进入不一致状态。如果某个关键操作失败,取消整个关联操作远比让其他部分继续运行更安全。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Starting the scope...")
    try {
        coroutineScope { // 创建新作用域
            launch {
                try {
                    println("Child 1: Working for 1000ms...")
                    delay(1000)
                    println("Child 1: Finished.") // 不会执行
                } finally {
                    println("Child 1: I was cancelled!")
                }
            }

            launch {
                println("Child 2: Working for 500ms then failing...")
                delay(500)
                throw RuntimeException("Child 2 failed!")
            }
        }
    } catch (e: Exception) {
        // Child 2 的异常向上传播到父作用域并被捕获
        println("Caught exception in parent scope: ${e.message}")
    }
    println("Scope finished.")
}

Starting the scope...
Child 1: Working for 1000ms...
Child 2: Working for 500ms then failing...
Child 1: I was cancelled!
Caught exception in parent scope: Child 2 failed!
Scope finished.

可以看到,Child 2 失败后,Child 1 还没完成工作就被立即取消了。

嵌套作用域的异常传播

在协程作用域内嵌套另一个 coroutineScope 时,内层作用域会遵循同样的“失败即取消”规则。

如果父作用域失败,内层作用域会被取消;反之,内层作用域的任意子协程失败,也会导致内层所有子协程取消,并将异常传播到父作用域。这种嵌套结构确保了整个任务树的一致性和可控性。

fun main() = runBlocking {
    println("Starting the scope...")
    launch {
        println("Parent: Working for 500ms then failing...")
        delay(500)
        // 抛出一个上层异常
        throw RuntimeException("Parent failed!")
    }
    try {
        coroutineScope { // 创建新作用域
            launch {
                try {
                    println("Child 1: Working for 1000ms...")
                    delay(1000)
                    println("Child 1: Finished.") // 不会执行
                } finally {
                    println("Child 1: I was cancelled!")
                }
            }
        }
    } catch (e: Exception) {
        // 上层异常导致协程取消并被捕获
        println("Caught exception in scope: ${e.message}")
    }
}
Starting the scope...
Parent: Working for 500ms then failing...
Child 1: Working for 1000ms...
Child 1: I was cancelled!
Caught exception in scope: Parent job is Cancelling
...

场景四:隔离

1.jpg

如果希望某个子协程的失败不影响其他兄弟协程,就用 supervisorScope——它会覆盖父作用域的取消策略。

使用思路

supervisorScope 中直接子协程的异常,不会触发父作用域或兄弟协程的取消。这种特性在 UI 应用中特别实用,比如多个独立任务并行运行时,一个任务出错不该导致整个界面卡死。

重要提示:监督机制仅对直接子协程生效。

这一点我发现很多开发者包括我的同事也忽略了!你可以这样记住:

爸爸只管儿子,管不着孙子!

孙子谁管?儿子管!

当然,如果在 supervisorScope 内部嵌套 coroutineScope,这个内层作用域仍遵循“失败即取消”规则——其内部任意子协程失败,都会导致内层所有子协程被取消。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Starting the supervisor scope...")
    try {
        supervisorScope { // 子协程失败隔离
            launch {
                println("Child 1: Working for 500ms then failing...")
                delay(500)
                throw RuntimeException("Child 1 failed!")
            }

            launch {
                try {
                    println("Child 2: Working for 1000ms...")
                    delay(1000)
                    println("Child 2: Finished successfully!") // 会执行
                } finally {
                    println("Child 2: I was NOT cancelled!")
                }
            }
        }
    } catch (e: Exception) {
        // ❌ 不会执行,supervisorScope 不传播子协程异常
        println("Caught exception in parent scope: $e")
    }
    println("Supervisor scope finished.")
}

Starting the supervisor scope...
Child 1: Working for 500ms then failing...
Child 2: Working for 1000ms...
Exception in thread "main" java.lang.RuntimeException: Child 1 failed!
... (堆栈信息) ...
Child 2: Finished successfully!
Child 2: I was NOT cancelled!
Supervisor scope finished.

这里程序依然崩溃了——原因是 supervisorScope 只阻止取消传播(也就是不会取消其他儿子的任务),不会自动处理异常。

Child 1 抛出的异常未被捕获,最终导致主线程崩溃。因此,必须在 supervisorScope 的子协程内部手动处理异常。

正确使用 supervisorScope

supervisorScope 的子协程内部添加 try-catch,手动处理异常。

再次用到古法捕获!你不要管老不老,关键是好使!


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Starting the supervisor scope...")
    supervisorScope {
        // Child 1 自行处理异常
        launch {
            try {
                println("Child 1: I'm going to fail.")
                delay(500)
                throw RuntimeException("Child 1 failed!")
            } catch (e: Exception) {
                println("Caught in Child 1: ${e.message}")
            }
        }

        // Child 2 独立运行,不受影响
        launch {
            println("Child 2: I will succeed.")
            delay(1000)
            println("Child 2: Finished successfully!")
        }
    }
    println("Supervisor scope finished.")
}

Starting the supervisor scope...
Child 1: I'm going to fail.
Child 2: I will succeed.
Caught in Child 1: Child 1 failed!
Child 2: Finished successfully!
Supervisor scope finished.

此时运行正常:Child 1 失败后自行处理了错误,Child 2 不受影响并顺利完成任务,同时也不会影响程序运行,一切照常!

场景五:全局异常捕获

CoroutineExceptionHandler 是协程异常处理的“最后防线”,它是一个特殊的上下文元素,可添加到顶层作用域,用于捕获所有未被其他方式处理的异常。

适用场景

主要用于日志记录、错误上报或清理未处理异常,特别适合与 GlobalScopeSupervisorJob 配合使用。

无效场景

对普通 coroutineScope 的子协程无效——因为 coroutineScope 会在子协程失败时主动取消并处理异常,不会让异常冒泡到顶层处理器。

但对 async 有效,因为其异常被封装在 Deferred 中,直到 await() 才暴露。


import kotlinx.coroutines.*

fun main() = runBlocking {
    // 1. 创建异常处理器
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught by CoroutineExceptionHandler: $exception")
    }

    // 2. 创建带处理器的 SupervisorJob 作用域
    val scope = CoroutineScope(SupervisorJob() + handler)

    // 3. 启动会失败的协程
    scope.launch {
        println("Child 1: Failing...")
        throw AssertionError("Something is wrong!")
    }

    // 4. 启动不受影响的协程
    scope.launch {
        delay(500)
        println("Child 2: I'm alive!")
    }

    // 等待协程执行
    delay(1000)
    println("Main finished.")
}

Child 1: Failing...
Caught by CoroutineExceptionHandler: java.lang.AssertionError: Something is wrong!
Child 2: I'm alive!
Main finished.

处理器成功捕获了 Child 1 的异常,同时 Child 2 不受影响正常运行,且程序也正常运行,没有崩溃——这正是 SupervisorJob 隔离失败的特性与 CoroutineExceptionHandler 全局捕获的结合效果。

场景六:supervisorScope 中使用 async

supervisorScope 隔离失败的规则对 async 同样适用:某个 async 子协程失败,不会影响其他兄弟协程(无论 launch 还是 async)。

async 的异常仍会暂存于 Deferred 对象中,需在调用 await() 时手动处理。

使用思路

这种组合适合多个独立的异步任务并行执行的场景。即使某个任务失败,其他任务仍能正常完成,且失败任务的异常可在获取结果时单独处理。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Starting supervisor scope with async...")
    supervisorScope {
        // 会失败的 async 任务
        val deferredFailure = async {
            println("Async 1: I will fail in 500ms.")
            delay(500)
            throw IllegalStateException("Failure!")
        }

        // 会成功的 async 任务
        val deferredSuccess = async {
            println("Async 2: I will succeed in 1000ms.")
            delay(1000)
            "Success!"
        }

        // 处理失败任务的异常
        try {
            deferredFailure.await()
        } catch (e: Exception) {
            println("Caught expected failure from Async 1: ${e.message}")
        }

        // 获取成功任务的结果
        try {
            val result = deferredSuccess.await()
            println("Result from Async 2: $result")
        } catch (e: Exception) {
            println("Caught unexpected failure from Async 2: $e")
        }
    }
    println("Supervisor scope finished.")
}

Starting supervisor scope with async...
Async 1: I will fail in 500ms.
Async 2: I will succeed in 1000ms.
Caught expected failure from Async 1: Failure!
Result from Async 2: Success!
Supervisor scope finished.

可以看到,deferredSuccess 未受 deferredFailure 异常影响,正常完成并返回结果;同时,deferredFailure 的异常在 await() 时被成功捕获。

场景七:取消是一种特殊的异常

协程被取消时,会抛出 CancellationException——这是一种特殊异常,通常会被协程机制自动忽略,无需手动捕获。

使用思路

CancellationException 代表协程正常取消,是结构化并发的常规行为。虽然可以用 try-catch 捕获它(比如用于日志记录),但不应“吞掉”它——若捕获后需要执行逻辑,务必重新抛出,确保取消流程完整。协程的清理逻辑(如关闭资源、释放连接),最佳位置是 finally 块。


import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            println("Job: I'm working...")
            delay(2000) // 挂起点,可响应取消
            println("Job: I'm done.") // 不会执行
        } catch (e: CancellationException) {
            // 日志记录,必须重新抛出
            println("Job: I was cancelled. Re-throwing exception.")
            throw e
        } catch (e: Exception) {
            println("Job: Caught some other exception: $e")
        } finally {
            // ✅ 清理逻辑的正确位置
            println("Job: Finally block executed for cleanup.")
        }
    }

    delay(1000)
    println("Main: I'm tired of waiting, cancelling the job.")
    job.cancelAndJoin() // 取消并等待协程完成
    println("Main: Job has been cancelled.")
}

Job: I'm working...
Main: I'm tired of waiting, cancelling the job.
Job: I was cancelled. Re-throwing exception.
Job: Finally block executed for cleanup.
Main: Job has been cancelled.

场景八:不可取消的清理

如果 finally 块中的清理逻辑包含挂起函数(比如写文件、网络请求),协程被取消后,这些挂起调用会立即抛出 CancellationException,导致清理逻辑无法完整执行。

解决方案

finally 块中,通过 withContext(NonCancellable) 切换上下文。

NonCancellable 能保证块内代码完整执行,不受协程取消状态影响。


import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        try {
            println("Job: Working...")
            delay(2000)
        } finally {
            println("Job: Entering finally block.")
            // 切换到 NonCancellable 上下文执行挂起清理逻辑
            withContext(NonCancellable) {
                println("Job: Starting a suspending cleanup that takes 500ms...")
                delay(500) // 模拟耗时清理
                println("Job: Crucial cleanup finished.")
            }
        }
    }

    delay(1000)
    println("Main: Cancelling the job.")
    job.cancelAndJoin()
    println("Main: Job cancelled.")
}

Job: Working...
Main: Cancelling the job.
Job: Entering finally block.
Job: Starting a suspending cleanup that takes 500ms...
Job: Crucial cleanup finished.
Main: Job cancelled.

即使协程被取消,NonCancellable 块内的 delay(500) 仍能顺利执行,确保清理逻辑完成。

如果你需要在协程被取消之后执行一个清理任务,但是这个清理任务又是一个可挂起的函数,那么 NonCancellable 就再适合不过了。

场景九:嵌套作用域与异常传播细节

coroutineScope(失败即取消)和 supervisorScope(隔离失败)的规则,仅对直接子协程生效。

嵌套结构中,内层作用域的规则不会被外层覆盖——比如 supervisorScope 内的 coroutineScope,仍遵循“内部失败即全取消”。


import kotlinx.coroutines.*

fun main() = runBlocking {
    // 顶层 supervisorScope:隔离直接子协程失败
    supervisorScope {
        // 直接子协程 1:正常运行
        launch {
            println("Supervisor's Child 1: I survived!")
        }

        // 直接子协程 2:包含内层 coroutineScope
        launch {
            coroutineScope { // 内层遵循“失败即取消”
                println("Inner Scope Child: I'm about to fail!")
                delay(500)
                throw RuntimeException("Failure in inner scope")
            }
        }
    }
    println("All done.")
}
输出结果

Inner Scope Child: I'm about to fail!
Supervisor's Child 1: I survived!
Exception in thread "main" java.lang.RuntimeException: Failure in inner scope
...

关键结论:Supervisor's Child 1 未受影响(体现 supervisorScope 隔离特性);内层 coroutineScope 的子协程失败后,内层作用域自身被取消(体现 coroutineScope 规则);未捕获的异常最终导致程序崩溃。

场景十:任务层级详解

结构化并发的核心是“任务层级(Job Hierarchy)”,可以形象理解为一棵任务树:

  • 在协程(或作用域)中启动新协程时,新协程会成为父协程的子任务;

  • 父任务有两个核心责任:一是等待所有子任务完成后才会自身完成;二是若子任务失败(非 supervisor 场景),则取消所有其他子任务并自身失败。

这种层级关系确保了协程的“结构化”,避免任务失控。


import kotlinx.coroutines.*

fun main() = runBlocking {
    // runBlocking 是顶层父作用域
    println("Parent Scope: I'm the parent job.")
    val job1 = launch {
        println("Child Job 1: I'm a child of the parent scope.")
        delay(1000)
        println("Child 1: I'm done.")
    }

    val job2 = launch {
        println("Child Job 2: I'm also a child.")
        delay(500)
        // 创建孙协程(job2 的子协程)
        launch {
            println("Grandchild: My parent is Child 2.")
            delay(500)
            println("Grandchild: I'm done.")
        }
        println("Child 2: I'm done.")
    }

    println("Parent Scope: Waiting for my children to finish.")
    // runBlocking 会等待所有子任务(job1、job2、孙协程)完成后才结束
}
输出结果

Parent Scope: I'm the parent job.
Parent Scope: Waiting for my children to finish.
Child Job 1: I'm a child of the parent scope.
Child Job 2: I'm also a child.
Grandchild: My parent is Child 2.
Grandchild: I'm done.
Child 2: I'm done.
Child 1: I'm done.

可见,runBlocking 作为父作用域,会等待所有子协程(包括嵌套的孙协程)完成后才结束,完全遵循任务层级的约束。

场景十一:supervisorScope vs CoroutineScope(SupervisorJob())

两者都基于 SupervisorJob 实现失败隔离,但适用场景不同:

  • supervisorScope { ... }:用于创建“局部隔离区”,适合隔离一组相关但独立的临时任务。它是一个挂起函数(这意味着它的运行需要一个协程域),会等待所有直接子协程完成;若子协程异常未被捕获,会抛出到外部作用域。

  • CoroutineScope(SupervisorJob()):创建一个可复用的作用域对象(它本身就是在创建一个协程域),适合组件化架构(如 Android ViewModel、独立模块)。作用域内的直接子协程相互隔离,且作用域不会自动结束,需显式调用 cancel() 销毁。


import kotlinx.coroutines.*

class Component {
    // 创建带 SupervisorJob 和异常处理器的作用域
    private val scope = CoroutineScope(SupervisorJob() + CoroutineExceptionHandler { _, e ->
        println("Component: Caught an error: $e")
    })

    // 启动高风险任务
    fun startRiskyTask() {
        scope.launch {
            println("Risky Task: Starting...")
            delay(1000)
            throw RuntimeException("Something went wrong")
        }
    }

    // 启动稳定任务
    fun startStableTask() {
        scope.launch {
            println("Stable Task: I'm running...")
            delay(1000)
            println("Stable Task: I finished successfully.")
        }
    }

    // 组件销毁时取消作用域
    fun destroy() {
        println("Component: Destroying and cancelling scope.")
        scope.cancel()
    }
}

fun main() = runBlocking {
    val component = Component()
    component.startRiskyTask()
    component.startStableTask()
    delay(2000) // 等待任务执行
    component.destroy()
    delay(500)
}

Risky Task: Starting...
Stable Task: I'm running...
Component: Caught an error: java.lang.RuntimeException: Something went wrong
Stable Task: I finished successfully.
Component: Destroying and cancelling scope.

即使高风险任务失败,稳定任务仍正常完成;作用域保持活跃直到显式销毁,符合组件化开发的生命周期管理需求。

场景十二:处理超时

长时间运行的任务可能导致资源占用或响应缓慢,协程提供了简洁的超时控制方案:

  • withTimeout(timeoutMillis) { ... }:执行代码块,若超时则抛出 TimeoutCancellationException,同时取消协程;

  • withTimeoutOrNull(timeoutMillis) { ... }:非抛出版本,超时后返回 null,不会抛出异常,代码更简洁易维护。


import kotlinx.coroutines.*

fun main() = runBlocking {
    // 使用 withTimeout(抛出版)
    try {
        withTimeout(1000) {
            println("Task 1: I have a second to complete.")
            delay(2000) // 会超时
            println("Task 1: I finished.") // 不会执行
        }
    } catch (e: TimeoutCancellationException) {
        println("Task 1 failed: ${e.message}")
    }

    // 使用 withTimeoutOrNull(非抛出版)
    val result = withTimeoutOrNull(1000) {
        println("Task 2: Trying to complete in 1 second.")
        delay(2000)
        "Success"
    }

    if (result == null) {
        println("Task 2 timed out and returned null.")
    } else {
        println("Task 2 finished with result: $result")
    }
}

Task 1: I have a second to complete.
Task 1 failed: Timed out waiting for 1000 ms
Task 2: Trying to complete in 1 second.
Task 2 timed out and returned null.

实际开发中,withTimeoutOrNull 更常用,可避免额外的 try-catch 嵌套。

场景十三:等待多个异步任务的异常处理

当需要等待多个 async 任务完成时,推荐使用 awaitAll()——它遵循“全或无”原则,与 coroutineScope 逻辑一致:若任意一个任务失败,立即取消所有其他任务,并抛出第一个失败的异常。


import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Starting multiple async jobs.")
    try {
        coroutineScope {
            val deferred1 = async {
                delay(1000)
                println("Job 1: Success.")
                "Result 1"
            }

            val deferred2 = async {
                delay(500)
                println("Job 2: Failing!")
                throw IllegalStateException("Job 2 Error")
            }

            // 等待所有任务完成,任意失败则立即抛出异常
            val results = awaitAll(deferred1, deferred2)
            println("All jobs finished: $results") // 不会执行
        }
    } catch (e: Exception) {
        println("Caught exception in awaitAll: ${e.message}")
    }
}

Starting multiple async jobs.
Job 2: Failing!
Caught exception in awaitAll: Job 2 Error

可以看到,Job 2 失败后,awaitAll() 立即抛出异常,Job 1 被取消(其延迟 1000ms 的任务未完成,因此“Job 1: Success.” 未打印)。

总结

以上场景覆盖了协程异常处理的核心场景和最佳实践。

屏幕前的你,现在强的可怕!

异常处理的核心原则是:利用结构化并发的层级特性,让异常处理逻辑紧跟异常发生位置,根据任务关联性选择合适的作用域(coroutineScope/supervisorScope),必要时用 CoroutineExceptionHandler 兜底。

实在不行,古法捕获!

掌握这些逻辑,就能让协程的异常处理既安全又优雅。

我把这篇文章甩给我的同事,我相信他以后绝对不会问我异常处理的问题了。