クイズで理解度を確かめよう!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問の、CoroutineExceptionHandler
をlaunch
に与えていないバージョンです。
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マスターです。ここで引き返していただいて問題ありません。
一方で、分からない問題があった方は、ぜひ以降の解説を読み、これを機に例外処理に関する理解を深めていただければと思います。
例外が投げられる条件
クイズの解説に入る前に、例外処理の流れを解説します。
コルーチン内で例外が投げられると、以下のようなルールに沿って、例外が処理されます。
-
launch
とasync
の両方で、親コルーチン (launch
やasync
) が存在する場合、例外は親に伝播する。この際、CoroutineExceptionHandler
は使われない。 -
launch
とasync
の両方で、親がcoroutineScope
の場合には、coroutineScope
から例外が投げられる。 -
launch
のみ、「親がsupervisorScope
」あるいは「親が存在しない場合」には、CoroutineExceptionHandler
によって例外が処理される。 -
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("③")
}
}
答え
③
解説
親がcoroutineScope
なので、coroutineScope
から例外が投げられます。
launch
自体からは例外は投げられません。
また、例外を親に伝播できる場合 (coroutineScope
やlaunch
、async
が親として存在する場合) には、launch
に渡されたCoroutineExceptionHandler
は使用されません。
親コルーチンが存在する場合にCoroutineExceptionHandler
が使われない理由を、内部実装からも確認してみましょう。
JobSupport
のfinalizeFinishingState
[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("③")
}
}
答え
①
解説
親がsupervisorScope
なので、親コルーチンに例外処理を委譲できず、CoroutineExceptionHandler
が使われます。
supervisorScope
でCoroutineExceptionHandler
が使われる理由を、内部実装からも確認します。
第1問の解説で述べたように、cancelParent
がtrueを返すと、親コルーチンに例外処理が委譲され、CoroutineExceptionHandler
の利用がスキップされることとなります。
JobSupport
のcancelParent
のソースコード[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("②")
}
}
答え
①
解説
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("③")
}
}
答え
②
③
解説
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("③")
}
}
答え
②
解説
第4問と同様に、await
した際に例外が投げられます。
一方で、親がsupervisorScope
のため、supervisorScope
へは例外が伝播せず、そこからは例外が投げられません。
また、launch
との大きな違いとして、CoroutineExceptionHandler
は呼ばれません。
async
でCoroutineExceptionHandler
が使われない理由を、内部実装からも確認してみます。
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("③")
}
}
答え
②
解説
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("④")
}
}
答え
④
解説
親コルーチンへと例外が伝播したのち、最終的に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]を見ると、CoroutineContext
にCoroutineExceptionHandler
が存在する場合にはそれが使用され、存在しない場合には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("②")
}
}
答え
①
解説
フローチャートには含まれないパターンです。
内部のcoroutineScope
から外部coroutineScope
への、コルーチン (Job
) の親子構造を通じた例外の伝播は行われません。そのため、内部のcoroutineScope
から投げられた例外を補足すれば、外部のcoroutineScope
へは例外が伝播しません。
仮に内部のcoroutineScope
から投げられた例外を補足しなければ、その側のcoroutineScope
から再び投げられることになります。
-
JobSupport
のfinalizeFinishingState
: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L222 ↩︎ -
JobSupport
のcancelParent
: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/JobSupport.kt#L222 ↩︎ -
SupervisorCoroutine
: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Supervisor.kt#L64 ↩︎ -
The reason to avoid GlobalScope: https://elizarov.medium.com/the-reason-to-avoid-globalscope-835337445abc ↩︎
-
Coroutine exceptions handling: https://kotlinlang.org/docs/exception-handling.html ↩︎
-
StandaloneCoroutine
: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/Builders.common.kt#L187 ↩︎ -
handleCoroutineException
: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt#L18 ↩︎ -
handleUncaughtCoroutineException
: https://github.com/Kotlin/kotlinx.coroutines/blob/f4f519b36734238ec686dfaec1e174086691781e/kotlinx-coroutines-core/common/src/internal/CoroutineExceptionHandlerImpl.common.kt#L30 ↩︎
Discussion