🤯

クイズで理解度を確かめよう!Kotlin Coroutinesの例外処理のフローチャート

に公開

本記事では、「Kotlin Coroutinesの例外処理」について解説します。
解説に入る前に、現時点での理解度を確かめるために、まずはクイズを解いてみましょう!

クイズ

クイズは全10問です。
それぞれのサンプルコードを実行した結果、得られる出力を答えて下さい。

第1問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, _ -> println("①") }
    try {
        coroutineScope {
            try {
                launch(handler) {
                    throw SomeException()
                }
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}
答え

第2問

第1問のcoroutineScopeを、supervisorScopeに変えたものです。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, _ -> println("①") }
    try {
        supervisorScope {
            try {
                launch(handler) {
                    throw SomeException()
                }
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}
答え

第3問

GlobalScopeからのlaunchでコルーチンを起動しています。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, _ -> println("①") }
    try {
        val job = GlobalScope.launch(handler) {
            throw SomeException()
        }
        job.join()
    } catch (e: SomeException) {
        println("②")
    }
}

答え

第4問

launchの代わりに、asyncを使用しています。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, e -> println("①") }
    try {
        coroutineScope {
            val result = async(handler) {
                throw SomeException()
            }
            try {
                result.await()
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}
答え

第5問

第4問のcoroutineScopeを、supervisorScopeに変えたものです。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, e -> println("①") }
    try {
        supervisorScope {
            val result = async(handler) {
                throw SomeException()
            }
            try {
                result.await()
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}
答え

第6問

GlobalScopeからasyncを起動しています。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e -> println("①") }
    try {
        val result = GlobalScope.async(handler) {
            throw SomeException()
        }
        try {
            result.await()
        } catch (e: SomeException) {
            println("②")
        }
    } catch (e: SomeException) {
        println("③")
    }
}
答え

第7問

launchから、更にlaunchで子コルーチンが起動されています。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        coroutineScope {
            val handler = CoroutineExceptionHandler { _, _ -> println("①") }
            launch(handler) {
                val handler2 = CoroutineExceptionHandler { _, _ -> println("②") }
                launch(handler2) {
                    val handler3 = CoroutineExceptionHandler { _, _ -> println("③") }
                    launch(handler3) {
                        throw SomeException()
                    }
                }
            }
        }  
    } catch (e: SomeException) {
        println("④")
    }
}
答え

第8問

asyncから、更にasyncで子コルーチンが起動されています。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        coroutineScope {
            val r1 = async {
                val r2 = async {
                    val r3 = async {
                        throw SomeException()
                    }
                    try {
                        r3.await()
                    } catch (e: SomeException) {
                        println("①")
                    }
                }
                try {
                    r2.await()
                } catch (e: SomeException) {
                    println("②")
                }
            }
            try {
                r1.await()
            } catch (e: SomeException) {
                println("③")
            }
        }
    } catch (e: SomeException) {
        println("④")
    }
}
答え

※①②③の出力される順番は非決定的である。
▶️ Kotlin Playgroundで試す

①
②
③
④

第9問

第2問の、CoroutineExceptionHandlerlaunchに与えていないバージョンです。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        supervisorScope {
            try {
                launch {
                    throw SomeException()
                }
            } catch (e: SomeException) {
                println("①")
            }
        }  
    } catch (e: SomeException) {
        println("②")
    }
}
答え

※この問題の出力は、実行プラットフォームに依存します。Kotlin Playground上では、以下のような出力となります。
▶️ Kotlin Playgroundで試す

Exception in thread "DefaultDispatcher-worker-1 @coroutine#1" SomeException: 
	at FileKt$main$2$1.invokeSuspend(File.kt:10)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}@435ce98, Dispatchers.Default]

第10問

coroutineScope内にcoroutineScopeが存在するケースです。

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        coroutineScope {
            try {
                coroutineScope {
                    launch {
                        throw SomeException()
                    }
                }  
            } catch (e: SomeException) {
                println("①")
            }
        }  
    } catch (e: SomeException) {
        println("②")
    }
}
答え

お疲れ様でした。クイズは以上です。

「全問正解できた」かつ「その理由を説明できる」という方は、既にCoroutinesマスターです。ここで引き返していただいて問題ありません。

一方で、分からない問題があった方は、ぜひ以降の解説を読み、これを機に例外処理に関する理解を深めていただければと思います。

例外が投げられる条件

クイズの解説に入る前に、例外処理の流れを解説します。

コルーチン内で例外が投げられると、以下のようなルールに沿って、例外が処理されます。

  1. launchasyncの両方で、親コルーチン (launchasync) が存在する場合、例外は親に伝播する。この際、CoroutineExceptionHandlerは使われない
  2. launchasyncの両方で、親がcoroutineScopeの場合には、coroutineScopeから例外が投げられる
  3. launchのみ、「親がsupervisorScope」あるいは「親が存在しない場合」には、CoroutineExceptionHandlerによって例外が処理される。
  4. asyncのみ、asyncの返り値 (Deferredインスタンス) に対してawaitを呼び出した際に例外が投げられる

これらのルールをフローチャートに整理すると、以下のようになります。


例外が投げられる条件のフローチャート

クイズの解説

各問題を、先述したルール・フローチャートに従って解説していきます。

第1問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, _ -> println("①") }
    try {
        coroutineScope {
            try {
                launch(handler) {
                    throw SomeException()
                }
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

親がcoroutineScopeなので、coroutineScopeから例外が投げられます。
launch自体からは例外は投げられません。
また、例外を親に伝播できる場合 (coroutineScopelaunchasyncが親として存在する場合) には、launchに渡されたCoroutineExceptionHandlerは使用されません

親コルーチンが存在する場合にCoroutineExceptionHandlerが使われない理由を、内部実装からも確認してみましょう。
JobSupportfinalizeFinishingState[1]に、以下のようなコードがあります。

val handled = cancelParent(finalException) || handleJobException(finalException)

cancelParentメソッドは、親コルーチンが例外処理の責務を持つ場合にtrueを返します。
また、handleJobException内で、CoroutineExceptionHandlerを使った例外処理が行われます。つまり、親コルーチンに例外処理が委譲可能な場合、CoroutineExceptionHandlerの使用がスキップされることが分かります。

第2問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, _ -> println("①") }
    try {
        supervisorScope {
            try {
                launch(handler) {
                    throw SomeException()
                }
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

親がsupervisorScopeなので、親コルーチンに例外処理を委譲できず、CoroutineExceptionHandlerが使われます

supervisorScopeCoroutineExceptionHandlerが使われる理由を、内部実装からも確認します。

第1問の解説で述べたように、cancelParentがtrueを返すと、親コルーチンに例外処理が委譲され、CoroutineExceptionHandlerの利用がスキップされることとなります。

JobSupportcancelParentのソースコード[2]を以下に示します。

    private fun cancelParent(cause: Throwable): Boolean {
        if (isScopedCoroutine) return true

        val isCancellation = cause is CancellationException
        val parent = parentHandle
        // No parent -- ignore CE, report other exceptions.
        if (parent === null || parent === NonDisposableHandle) {
            return isCancellation
        }

        return parent.childCancelled(cause) || isCancellation
    }

親コルーチン (のJob) に対してparent.childCancelledが呼ばれています。

supervisorScopeで使用されるSupervisorCoroutineを見ると、以下のようにchildCancelledメソッドがoverrideされており、falseを返すのみとなっています[3]。そのため、cancelParentがfalseを返し、CoroutineExceptionHandlerを使った例外処理が行われることとなります。

private class SupervisorCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

第3問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, _ -> println("①") }
    try {
        val job = GlobalScope.launch(handler) {
            throw SomeException()
        }
        job.join()
    } catch (e: SomeException) {
        println("②")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

GlobalScopeからコルーチンを起動すると親コルーチン (親のJob) が存在しない状態となります。そのため、CoroutineExceptionHandlerが使われます。

第4問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, e -> println("①") }
    try {
        coroutineScope {
            val result = async(handler) {
                throw SomeException()
            }
            try {
                result.await()
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}

答え

▶️ Kotlin Playgroundで試す

②
③

解説

awaitの場合、返り値のDeferredに対してawaitした際に例外が投げられます。
一方で、親コルーチンへの例外の伝播も起こるため、coroutineScopeからも例外が投げられます。

第5問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, e -> println("①") }
    try {
        supervisorScope {
            val result = async(handler) {
                throw SomeException()
            }
            try {
                result.await()
            } catch (e: SomeException) {
                println("②")
            }
        }  
    } catch (e: SomeException) {
        println("③")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

第4問と同様に、awaitした際に例外が投げられます。
一方で、親がsupervisorScopeのため、supervisorScopeへは例外が伝播せず、そこからは例外が投げられません。
また、launchとの大きな違いとして、CoroutineExceptionHandlerは呼ばれません

asyncCoroutineExceptionHandlerが使われない理由を、内部実装からも確認してみます。
launchではStandaloneCoroutineが用いられ、以下のようにhandleJobExceptionメソッドがoverrideされています[6]

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

このhandleJobExceptionは、親コルーチンに例外処理を委譲できなかった場合 (cancelParentがfalseを返した場合) に呼ばれるメソッドです。このメソッド内のhandleCoroutineExceptionで、CoroutineExceptionHandlerを使った例外処理が行われます。

他方、asyncでは、DeferredCoroutineが使われます。

private open class DeferredCoroutine<T>(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<T>(parentContext, true, active = active), Deferred<T> {
    override fun getCompleted(): T = getCompletedInternal() as T
    override suspend fun await(): T = awaitInternal() as T
    override val onAwait: SelectClause1<T> get() = onAwaitInternal as SelectClause1<T>
}

こちらはhandleJobExceptionの実装が存在せず、基底クラスのJobSupportにある実装 (falseを返すのみ) が使われることとなります。
そのため、asyncではCoroutineExceptionHandlerが使用されません
await時に例外を捕捉できるため、そちらで例外処理をすべきという設計上の意図と思われます。

第6問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, e -> println("①") }
    try {
        val result = GlobalScope.async(handler) {
            throw SomeException()
        }
        try {
            result.await()
        } catch (e: SomeException) {
            println("②")
        }
    } catch (e: SomeException) {
        println("③")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

GlobalScopeから起動されたコルーチンには、親コルーチンが存在しないため、親へと例外が伝播せず、単にawait時に例外が投げられるのみとなります。

第7問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        coroutineScope {
            val handler = CoroutineExceptionHandler { _, _ -> println("①") }
            launch(handler) {
                val handler2 = CoroutineExceptionHandler { _, _ -> println("②") }
                launch(handler2) {
                    val handler3 = CoroutineExceptionHandler { _, _ -> println("③") }
                    launch(handler3) {
                        throw SomeException()
                    }
                }
            }
        }  
    } catch (e: SomeException) {
        println("④")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

親コルーチンへと例外が伝播したのち、最終的にcoroutineScopeから例外が投げられます。

第8問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        coroutineScope {
            val r1 = async {
                val r2 = async {
                    val r3 = async {
                        throw SomeException()
                    }
                    try {
                        r3.await()
                    } catch (e: SomeException) {
                        println("①")
                    }
                }
                try {
                    r2.await()
                } catch (e: SomeException) {
                    println("②")
                }
            }
            try {
                r1.await()
            } catch (e: SomeException) {
                println("③")
            }
        }
    } catch (e: SomeException) {
        println("④")
    }
}

答え

※①②③の出力される順番は非決定的である。
▶️ Kotlin Playgroundで試す

①
②
③
④

解説

第7問のlaunchの場合と同様に、親コルーチンへと例外が伝播したのち、最終的にcoroutineScopeから例外が投げられます。
また、それぞれのawaitの結果をawaitしたタイミングでも、例外が投げられます。
たとえawaitで例外を捕捉しても、親コルーチンへの例外の伝播は生じることは覚えておきましょう。

第9問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        supervisorScope {
            try {
                launch {
                    throw SomeException()
                }
            } catch (e: SomeException) {
                println("①")
            }
        }  
    } catch (e: SomeException) {
        println("②")
    }
}

答え

※この問題の出力は、実行プラットフォームに依存します。Kotlin Playground上では、以下のような出力となります。
▶️ Kotlin Playgroundで試す

Exception in thread "DefaultDispatcher-worker-1 @coroutine#1" SomeException: 
	at FileKt$main$2$1.invokeSuspend(File.kt:10)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
	Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}@435ce98, Dispatchers.Default]

解説

CoroutineExceptionHandlerが使われるべき条件で、明示的にCoroutineExceptionHandlerが設定されていない場合、デフォルトのハンドラが使用されます。

内部実装からも、この挙動を説明してみます。
handleCoroutineExceptionのソースコード[7]を見ると、CoroutineContextCoroutineExceptionHandlerが存在する場合にはそれが使用され、存在しない場合にはhandleUncaughtCoroutineExceptionメソッドにフォールバックすることが分かります。

public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
    val reportException = if (exception is DispatchException) exception.cause else exception
    // Invoke an exception handler from the context if present
    try {
        context[CoroutineExceptionHandler]?.let {
            it.handleException(context, reportException)
            return
        }
    } catch (t: Throwable) {
        handleUncaughtCoroutineException(context, handlerException(reportException, t))
        return
    }
    // If a handler is not present in the context or an exception was thrown, fallback to the global handler
    handleUncaughtCoroutineException(context, reportException)
}

handleUncaughtCoroutineExceptionのソースコードを以下に示します[8]

internal fun handleUncaughtCoroutineException(context: CoroutineContext, exception: Throwable) {
    // use additional extension handlers
    for (handler in platformExceptionHandlers) {
        try {
            handler.handleException(context, exception)
        } catch (_: ExceptionSuccessfullyProcessed) {
            return
        } catch (t: Throwable) {
            propagateExceptionFinalResort(handlerException(exception, t))
        }
    }

    try {
        exception.addSuppressed(DiagnosticCoroutineContextException(context))
    } catch (e: Throwable) {
        // addSuppressed is never user-defined and cannot normally throw with the only exception being OOM
        // we do ignore that just in case to definitely deliver the exception
    }
    propagateExceptionFinalResort(exception)
}

handleUncaughtCoroutineException内の処理は、実行プラットフォームに依存します。
プラットフォームごとに定義されたplatformExceptionHandlersに対してhandleExceptionが呼び出され、例外処理が行われます。

第10問

import kotlinx.coroutines.*

private class SomeException(): Exception("")

suspend fun main() {
    try {
        coroutineScope {
            try {
                coroutineScope {
                    launch {
                        throw SomeException()
                    }
                }  
            } catch (e: SomeException) {
                println("①")
            }
        }  
    } catch (e: SomeException) {
        println("②")
    }
}

答え

▶️ Kotlin Playgroundで試す

解説

フローチャートには含まれないパターンです。
内部のcoroutineScopeから外部coroutineScopeへの、コルーチン (Job) の親子構造を通じた例外の伝播は行われません。そのため、内部のcoroutineScopeから投げられた例外を補足すれば、外部のcoroutineScopeへは例外が伝播しません。
仮に内部のcoroutineScopeから投げられた例外を補足しなければ、その側のcoroutineScopeから再び投げられることになります。

脚注
  1. JobSupportfinalizeFinishingState: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L222 ↩︎

  2. JobSupportcancelParent: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L222 ↩︎

  3. SupervisorCoroutine: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Supervisor.kt#L64 ↩︎

  4. The reason to avoid GlobalScope: https://elizarov.medium.com/the-reason-to-avoid-globalscope-835337445abc ↩︎

  5. Coroutine exceptions handling: https://kotlinlang.org/docs/exception-handling.html ↩︎

  6. StandaloneCoroutine: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Builders.common.kt#L187 ↩︎

  7. handleCoroutineException: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt#L18 ↩︎

  8. handleUncaughtCoroutineException: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.common.kt#L30 ↩︎

Discussion