はじめに
― 「1つが失敗しても、全体を止めない並行処理」 ―
1. コルーチンのエラーハンドリングとは?
Kotlinのコルーチンでは、例外の扱い方がスレッドモデルと異なります。
例外はコルーチンのスコープ構造を通じて伝播します。
suspend fun taskA() {
throw Exception("Aで失敗!")
}
fun main() = runBlocking {
try {
launch { taskA() }
} catch (e: Exception) {
println("キャッチ: ${e.message}")
}
}
結果:
Exception in thread "main" java.lang.Exception: Aで失敗!
❗
launch内の例外は 外側の try/catch では拾えません。
launchは「非同期で動く別スコープ」だからです。
2. async と launch の違い
| 関数 | 戻り値 | 例外発生時の挙動 | 主な用途 |
|---|---|---|---|
launch |
Job |
直ちに親に伝播 | Fire-and-forget(実行だけ) |
async |
Deferred<T> |
await()時にスロー |
値を返す処理 |
例:
fun main() = runBlocking {
val job = launch {
throw Exception("launch例外")
}
val deferred = async {
throw Exception("async例外")
}
job.join() // ここではキャッチできない
deferred.await() // ここで初めて例外が発生
}
3. 構造化並行性と例外伝播の関係
coroutineScope の場合
suspend fun scopeExample() = coroutineScope {
launch { delay(500); println("B 完了") }
launch { throw Exception("A 失敗") }
}
出力:
Exception: A 失敗
一つの子コルーチンが失敗すると、
スコープ全体がキャンセルされる(他の子も停止)。
4. supervisorScope で独立させる
suspend fun supervisorExample() = supervisorScope {
launch {
throw Exception("A 失敗")
}
launch {
delay(500)
println("B 完了(継続)")
}
}
出力:
Exception: A 失敗
B 完了(継続)
supervisorScopeは 他の子タスクに影響を与えません。
→ 複数の独立タスクを安全に並行実行できます。
5. SupervisorJob でスコープ全体を監督化
アプリ全体のルートスコープを「Supervisor」に変えることで、
個別の失敗を隔離できます。
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun main() {
supervisorScope.launch {
launch {
throw Exception("A 失敗")
}
launch {
delay(500)
println("B 継続")
}
}
Thread.sleep(1000)
}
出力:
Exception: A 失敗
B 継続
SupervisorJobは「スコープの失敗耐性」を提供します。
6. CoroutineExceptionHandler でグローバル捕捉
例外が伝播する前に、
CoroutineExceptionHandler を使ってログ・通知処理を挟めます。
val handler = CoroutineExceptionHandler { _, e ->
println("例外捕捉: ${e.message}")
}
fun main() = runBlocking {
launch(handler) {
throw Exception("ネットワークエラー")
}
}
出力:
例外捕捉: ネットワークエラー
※
asyncの例外はawait()時に発生するため、
CoroutineExceptionHandlerでは捕捉されません。
7. 典型的なエラーハンドリング設計パターン
パターン①:すべて失敗時に停止(同期的)
suspend fun strictLoad() = coroutineScope {
val user = async { fetchUser() }
val orders = async { fetchOrders() }
println("${user.await()}, ${orders.await()}")
}
→ どちらかが失敗したら全体キャンセル。
パターン②:部分失敗を許容(非同期的)
suspend fun tolerantLoad() = supervisorScope {
val user = async { runCatching { fetchUser() }.getOrElse { "User失敗" } }
val orders = async { runCatching { fetchOrders() }.getOrElse { "Order失敗" } }
println("${user.await()}, ${orders.await()}")
}
出力例:
User失敗, 注文リスト
runCatching {}を組み合わせると、
タスク単位で例外を吸収して継続可能です。
パターン③:ViewModelやRepository層でのSupervisor設計
class MyViewModel : ViewModel() {
private val scope = viewModelScope + SupervisorJob()
fun loadAll() {
scope.launch {
launch { fetchUser() }
launch { fetchOrders() }
launch { fetchRecommendations() }
}
}
}
Androidでは
viewModelScopeが自動キャンセルを担い、
SupervisorJobが安全な並行性を保証します。
8. 実践Tips(よくある落とし穴)
| 問題 | 原因 | 解決策 |
|---|---|---|
| 例外が親に伝わらない |
async の中で未await()
|
await()で例外を発火させる |
| すべての子が止まる | 通常のcoroutineScopeを使用 |
supervisorScopeに変更 |
| try/catchで拾えない | launch内の例外 |
CoroutineExceptionHandlerを使用 |
| グローバルリーク |
GlobalScope利用 |
スコープを限定する(ViewModelScopeなど) |
まとめ
| 要点 | 内容 |
|---|---|
launch |
例外即伝播(値を返さない) |
async |
await()時に例外発生(値を返す) |
coroutineScope |
子の失敗で全体停止 |
supervisorScope / SupervisorJob
|
部分失敗を許容して継続 |
CoroutineExceptionHandler |
グローバル例外捕捉 |
| ベストプラクティス | スコープ境界を明確化+局所エラーハンドリング |