コルーチンの例外は「子 → 親」へ上がり、親は残りの子を全部キャンセルします。これを止めたいときが SupervisorJob・supervisorScope、最後に受けてログを出す係が CoroutineExceptionHandler です。 例外を捕まえて処理を続けたいなら、launch の中や coroutineScope の外側に try-catch を書きます。CoroutineExceptionHandler では処理を続けられません。

やりたいこと 書き方 実際の動き(後述の実行結果)
失敗を捕まえて続ける 中で try-catch、または coroutineScope { } の外で try-catch E10:外で捕まえ、呼び出し元は続いた
1つの子の失敗で兄弟を止めない supervisorScope { }・CoroutineScope(SupervisorJob()) E3:失敗しても兄弟は完了した
捕まえ損ねた例外をログに出す ルートの launch のスコープに CoroutineExceptionHandler E4(a)(e):ハンドラが受けた
async の失敗を受ける await() を try-catch E2:受けられるが、親も失敗する
キャンセルを正しく通す CancellationException は投げ直す E6・E7:握りつぶすとループが止まらない

確認環境:kotlinx-coroutines-core 1.8.0(kotlinc 2.4.20 に同梱の jar)、Kotlin 2.4.20、OpenJDK 21.0.10、Linux。この記事の E1〜E10 のコードを1つのファイルにまとめてコンパイル・実行し、出た出力をそのまま載せています。CoroutineExceptionHandler の仕様は、kotlinx.coroutines 1.8.0 のソースの KDoc と kotlinlang.org の「Coroutine exceptions handling」で確かめました。GitHub のタグで見た最新版は 1.11.0 です(確認日 2026年10月13日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlinコルーチン例外処理|SupervisorJobとHandlerの使い分け」

基本のルール:例外は親に上がり、親は兄弟を止める

コルーチンは親子の木になります(構造化された並行性、structured concurrency)。launch { } の中で launch { } すると、外側が親、内側が子です。kotlinlang.org の説明と kotlinx.coroutines の KDoc をまとめると、ルールは4つです。

ルール 内容
子の失敗は親へ 子が CancellationException 以外で失敗すると、親もその例外でキャンセルされる
親は兄弟を止める 親がキャンセルされると、まだ動いている子は全部キャンセルされる
キャンセルは親に上がらない cancel() や CancellationException で終わった子は、親を止めない
例外の処理はルートへ 子に付けた CoroutineExceptionHandler は使われず、ルート(一番上)で処理される

このルールのおかげで、「片方が失敗したのにもう片方が動き続けて、結果を捨てる」ことが起きません。逆に、「1つの失敗で全部止まる」のが困る場面(画面の中の独立した読み込みなど)では、SupervisorJob を使って上がる向きを止めます。

コルーチンの基本(launch・async・runBlocking)は、関連記事「Kotlinコルーチン入門|launch・async・runBlocking」で扱っています。

実行結果の早見表(E1〜E10)

この記事のコードを動かした結果の一覧です。各コードと出力は、このあと順に載せています。

No. 試したこと 結果
E1 launch を try-catch で囲む 捕まらない。runBlocking が投げた
E2 async の await() を try-catch catch に入るが、親も失敗した
E2b 同じことを supervisorScope の中で catch に入り、処理も続いた
E3 兄弟の片方が失敗(coroutineScope) もう片方はキャンセルされた
E3 兄弟の片方が失敗(supervisorScope) もう片方は完了した
E4 CoroutineExceptionHandler を6か所に付ける 効いたのはルートの launch だけ
E5 launch(SupervisorJob()) と書く 兄弟はキャンセルされた(よくある誤り)
E6 catch (e: Exception) で全部捕まえる cancel() してもループが最後まで回った
E7 suspend 関数を runCatching で包む キャンセルが Failure になり、処理が続いた
E8 2つの子が両方失敗 最初の例外が届き、後の例外は suppressed に入った
E9 子を cancel() する 親は動き続けた
E10 coroutineScope の外で try-catch 捕まえられ、呼び出し元は続いた

コードは次の import と、出力用の log 関数を前提にしています。

import kotlinx.coroutines.*
import java.io.IOException

fun log(msg: String) = println(msg)

launch と async で例外の出方が違う

E1:launch の例外は、launch を囲む try-catch では捕まらない

fun e1() {
    try {
        runBlocking {
            try {
                launch { throw IOException("launch で失敗") }
            } catch (e: Exception) {
                log("  launch の外の catch: $e")
            }
            delay(100)
            log("  ここは表示されない")
        }
    } catch (e: Exception) {
        log("  runBlocking の外で捕まえた: $e")
    }
}
== E1
  runBlocking の外で捕まえた: java.io.IOException: launch で失敗

launch を囲んだ catch には入りませんでした。 launch は子コルーチンを「始めるだけ」で、すぐに戻ります。例外は後で子の中で起き、親(runBlocking)へ上がり、runBlocking が呼び出し元へ投げました。launch の失敗を捕まえるには、launch { } の中で try-catch を書きます。

E2:async の例外は await で受けられるが、親も失敗する

fun e2() {
    try {
        runBlocking {
            val d = async { delay(10); throw IOException("async で失敗") }
            try { d.await() } catch (e: IOException) { log("  await の catch: $e") }
            log("  await の後の行(isActive=$isActive)")
            delay(100)
            log("  ここは表示されない")
        }
    } catch (e: Exception) {
        log("  runBlocking の外で捕まえた: $e")
    }
}
== E2
  await の catch: java.io.IOException: async で失敗
  await の後の行(isActive=false)
  runBlocking の外で捕まえた: java.io.IOException: async で失敗

await() の catch には入りましたが、runBlocking 自体も同じ例外で失敗しました。 async は例外を Deferred に入れて await() で投げ直します。一方で、async も子コルーチンなので、失敗は親にも上がります。isActive=false は、catch の時点で親がもうキャンセル中だったことを示します。次の delay(100) でキャンセルが効いて、後ろの行は動きませんでした。

E2b:supervisorScope の中なら、await の catch だけで済む

fun e2b() = runBlocking {
    supervisorScope {
        val d = async { delay(10); throw IOException("async で失敗") }
        try { d.await() } catch (e: IOException) { log("  await の catch: $e") }
        delay(100)
        log("  supervisorScope は続いている")
    }
}
== E2b
  await の catch: java.io.IOException: async で失敗
  supervisorScope は続いている

supervisorScope の直接の子は、失敗を親に上げません。async の失敗は await() だけで受けられ、処理は続きました。「async の失敗を await で受けて、代わりの値で続けたい」ときは、supervisorScope の中で async します。

coroutineScope と supervisorScope:兄弟が止まるかどうか

E3:片方の子が失敗したとき

suspend fun siblings(name: String, scope: suspend (suspend CoroutineScope.() -> Unit) -> Unit) {
    try {
        scope {
            launch { delay(50); throw IOException("子A 失敗") }
            launch {
                try { delay(200); log("  子B 完了") }
                catch (e: CancellationException) { log("  子B キャンセル: ${e::class.simpleName}"); throw e }
            }
        }
        log("  $name は正常に終わった")
    } catch (e: Exception) { log("  $name が投げた: $e") }
}

fun e3() = runBlocking {
    val h = CoroutineExceptionHandler { _, e -> log("  handler: $e") }
    log(" coroutineScope:")
    siblings("coroutineScope") { b -> coroutineScope { b() } }
    log(" supervisorScope(handler なし):")
    siblings("supervisorScope") { b -> supervisorScope { b() } }
    log(" supervisorScope + handler:")
    siblings("supervisorScope+handler") { b -> withContext(h) { supervisorScope { b() } } }
}
== E3
 coroutineScope:
  子B キャンセル: JobCancellationException
  coroutineScope が投げた: java.io.IOException: 子A 失敗
 supervisorScope(handler なし):
Exception in thread "main" java.io.IOException: 子A 失敗
    Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@7f560810, BlockingEventLoop@69d9c55]
  子B 完了
  supervisorScope は正常に終わった
 supervisorScope + handler:
  handler: java.io.IOException: 子A 失敗
  子B 完了
  supervisorScope+handler は正常に終わった

(スタックトレースの at … の行は省いています。)

coroutineScope supervisorScope
子Aの失敗で子Bは キャンセルされた 最後まで動いた
スコープ自体は 子Aの例外を投げた 正常に終わった
子Aの例外の行き先 スコープの呼び出し元(try-catch で捕まる) CoroutineExceptionHandler(無ければスレッドの既定の処理)
向いている場面 全部そろって初めて意味がある処理 独立した処理を並べる

supervisorScope で handler が無いと、子Aの例外は Exception in thread "main" として標準エラーに出るだけでした。 処理は止まりませんが、例外はどこにも記録されません。supervisorScope を使うときは、子の中で try-catch するか、CoroutineExceptionHandler を文脈に入れておきます。kotlinlang.org の説明でも、supervisorScope の直接の子は、ルートのコルーチンと同じように文脈の handler を使う、とされています。

CoroutineExceptionHandler が効く場所・効かない場所

CoroutineExceptionHandler は、誰にも捕まえられなかった例外を最後に受ける係です。kotlinx.coroutines 1.8.0 の KDoc には次のように書かれています。

  • 捕まらない例外は、ふつう launch で作ったルートのコルーチンからしか出ない
  • 子のコルーチンは処理を親に任せるので、子に付けた handler は使われない
  • SupervisorJob で動くコルーチンは、親に上げないのでルートと同じ扱い
  • async は例外を全部 Deferred に入れるので、捕まらない例外にならない
  • handler が呼ばれたとき、コルーチンはもう例外で終わっている。handler で立て直すことはできない

E4:6か所に付けて比べる

fun e4() {
    val h = CoroutineExceptionHandler { _, e -> log("  handler が受けた: $e") }
    log(" (a) CoroutineScope(Job()+h).launch:")
    runBlocking {
        val scope = CoroutineScope(Job() + h)
        scope.launch { throw IOException("a") }.join()
    }
    log(" (b) 子の launch に付けた:")
    runBlocking {
        val scope = CoroutineScope(Job())
        val old = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler { _, e -> log("  スレッドの既定ハンドラ: $e") }
        scope.launch { launch(h) { throw IOException("b") } }.join()
        Thread.setDefaultUncaughtExceptionHandler(old)
    }
    log(" (c) async に付けた:")
    runBlocking {
        val scope = CoroutineScope(Job() + h)
        val d = scope.async { throw IOException("c") }
        d.join()
        log("  何も起きない。await すると: " + runCatching { d.await() }.exceptionOrNull())
    }
    log(" (d) runBlocking に付けた:")
    try {
        runBlocking(h) { launch { throw IOException("d") } }
    } catch (e: Exception) { log("  handler は呼ばれず runBlocking が投げた: $e") }
    log(" (e) SupervisorJob のスコープの launch:")
    runBlocking {
        val scope = CoroutineScope(SupervisorJob() + h)
        scope.launch { throw IOException("e1") }.join()
        val j = scope.launch { log("  2つ目の launch も動く") }; j.join()
    }
    log(" (f) Job のスコープでは1回失敗するとスコープが終わる:")
    runBlocking {
        val scope = CoroutineScope(Job() + h)
        scope.launch { throw IOException("f1") }.join()
        val j = scope.launch { log("  ここは動かない") }; j.join()
        log("  scope.isActive=${scope.isActive}, 2つ目 isCancelled=${j.isCancelled}")
    }
}
== E4
 (a) CoroutineScope(Job()+h).launch:
  handler が受けた: java.io.IOException: a
 (b) 子の launch に付けた:
  スレッドの既定ハンドラ: java.io.IOException: b
 (c) async に付けた:
  何も起きない。await すると: java.io.IOException: c
 (d) runBlocking に付けた:
  handler は呼ばれず runBlocking が投げた: java.io.IOException: d
 (e) SupervisorJob のスコープの launch:
  handler が受けた: java.io.IOException: e1
  2つ目の launch も動く
 (f) Job のスコープでは1回失敗するとスコープが終わる:
  handler が受けた: java.io.IOException: f1
  scope.isActive=false, 2つ目 isCancelled=true
付けた場所 handler は 例外の行き先
(a) スコープ(Job())の launch 効いた handler
(b) 子の launch(h) 効かない ルートへ上がり、スレッドの既定ハンドラ
(c) async 効かない Deferred の中。await() で投げ直す
(d) runBlocking(h) 効かない runBlocking が呼び出し元へ投げる
(e) SupervisorJob() のスコープの launch 効いた handler。スコープは次の launch も動かせる
(f) Job() のスコープ 効いた handler。ただしスコープも終わり、次の launch は即キャンセル

(e) と (f) の違いが、アプリのスコープに SupervisorJob を使う理由です。 Job() のスコープは、子が1回失敗するとスコープごとキャンセルされ、その後の launch は動きませんでした。画面やサービスの寿命に合わせたスコープは CoroutineScope(SupervisorJob() + Dispatchers.Default + handler) の形にすると、1つの失敗で他の処理が止まりません。

E5:launch(SupervisorJob()) は兄弟を守らない

よくある誤りです。launch の引数に SupervisorJob() を渡しても、その中の子同士は守られません。

fun e5() = runBlocking {
    val h = CoroutineExceptionHandler { _, e -> log("  handler: $e") }
    launch(SupervisorJob() + h) {
        launch { delay(50); throw IOException("子A 失敗") }
        launch {
            try { delay(200); log("  子B 完了") }
            catch (e: CancellationException) { log("  子B キャンセルされた"); throw e }
        }
    }.join()
}
== E5
  子B キャンセルされた
  handler: java.io.IOException: 子A 失敗

子B はキャンセルされました。 launch(SupervisorJob()) は、「新しい SupervisorJob を親にして、ふつうの Job を持つコルーチンを1つ作る」という意味です。子A・子B の親は、その launch が作ったふつうの Job なので、子A の失敗で子B も止まります。さらに、runBlocking との親子関係も切れます(runBlocking はこの launch の終わりを待ちません。ここでは join() で待っています)。

書き方 子同士の関係
launch(SupervisorJob()) { launch{}; launch{} } 兄弟はキャンセルされる(E5)
supervisorScope { launch{}; launch{} } 兄弟は動き続ける(E3)
val scope = CoroutineScope(SupervisorJob()) → scope.launch{} を並べる それぞれ独立(E4 (e))

CancellationException を握りつぶす誤り

キャンセルは、delay() などの中断点で CancellationException を投げるしくみで動いています。catch (e: Exception) や catch (e: Throwable) はこれも捕まえるため、キャンセルが止まります。

E6:catch (e: Exception) のループ

fun e6() = runBlocking {
    log(" (誤) catch (e: Exception) で握りつぶす:")
    val job = launch(Dispatchers.Default) {
        var i = 0
        while (i < 5) {
            try { delay(100); log("  tick $i") } catch (e: Exception) { log("  catch: ${e::class.simpleName}") }
            i++
        }
        log("  ループを最後まで回った(isActive=$isActive)")
    }
    delay(150); log("  cancel()"); job.cancelAndJoin(); log("  join 完了")

    log(" (正) CancellationException は投げ直す:")
    val job2 = launch(Dispatchers.Default) {
        var i = 0
        while (i < 5) {
            try { delay(100); log("  tick $i") }
            catch (e: CancellationException) { throw e }
            catch (e: Exception) { log("  catch: $e") }
            i++
        }
        log("  ここは表示されない")
    }
    delay(150); log("  cancel()"); job2.cancelAndJoin(); log("  join 完了")
}
== E6
 (誤) catch (e: Exception) で握りつぶす:
  tick 0
  cancel()
  catch: JobCancellationException
  catch: JobCancellationException
  catch: JobCancellationException
  catch: JobCancellationException
  ループを最後まで回った(isActive=false)
  join 完了
 (正) CancellationException は投げ直す:
  tick 0
  cancel()
  join 完了

誤りの版は、cancel() の後もループを最後まで回りました。 キャンセル済みのコルーチンでは delay() がすぐに JobCancellationException を投げますが、それを毎回捕まえて次へ進んだためです。isActive=false なのに処理が続く、という状態になりました。この例は delay がすぐ戻るので短く終わりましたが、ループの中が通信や DB の処理なら、キャンセル後も重い処理が続きます。

直し方は3つあります。

直し方 書き方
先に投げ直す catch (e: CancellationException) { throw e } を広い catch より前に
捕まえる型を絞る catch (e: IOException) のように、想定する例外だけ
catch の後で確かめる catch (e: Exception) { currentCoroutineContext().ensureActive(); … }

ensureActive() は、キャンセル済みなら CancellationException を投げる関数です。Android Developers の「Best practices for coroutines in Android」にも、CancellationException は捕まえないか、捕まえたら必ず投げ直すこと、Exception・Throwable より IOException のような具体的な型を捕まえることが注意として書かれています。

runCatching の落とし穴

runCatching { } は Throwable を全部 Result に入れます。suspend 関数を包むと、E6 と同じことが起きます。

E7:キャンセルとタイムアウトが Failure になる

suspend fun fetch(): String { delay(100); return "data" }

fun e7() = runBlocking {
    log(" (a) runCatching が CancellationException を Result にする:")
    val job = launch {
        repeat(3) { n ->
            val r = runCatching { fetch() }
            log("  $n: $r")
        }
        log("  ループを最後まで回った")
    }
    delay(150); job.cancelAndJoin()

    log(" (b) withTimeout の TimeoutCancellationException:")
    val r = runCatching { withTimeout(50) { fetch() } }
    log("  $r")

    log(" (c) 外側が withTimeout だと:")
    val r2 = withTimeoutOrNull(250) {
        repeat(5) { n -> log("  $n: " + runCatching { fetch() }) }
        "完了"
    }
    log("  結果: $r2")
}
== E7
 (a) runCatching が CancellationException を Result にする:
  0: Success(data)
  1: Failure(kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@4678c730)
  2: Failure(kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@4678c730)
  ループを最後まで回った
 (b) withTimeout の TimeoutCancellationException:
  Failure(kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 50 ms)
 (c) 外側が withTimeout だと:
  0: Success(data)
  1: Success(data)
  2: Failure(kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 250 ms)
  3: Failure(kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 250 ms)
  4: Failure(kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 250 ms)
  結果: null
  • (a):キャンセルが Failure として返り、ループは最後まで回りました
  • (b):runCatching の中の withTimeout は、タイムアウトを Failure として受け取れました。これは意図どおりの使い方です
  • (c):外側の withTimeoutOrNull(250) の時間切れも、内側の runCatching が Failure にしていました。2〜4回目は「失敗した通信」と見分けがつかず、ループは最後まで回り、結果は null でした

E7b:CancellationException だけは投げ直す版

inline fun <T> runCatchingCancellable(block: () -> T): Result<T> =
    try { Result.success(block()) }
    catch (e: CancellationException) { throw e }
    catch (e: Throwable) { Result.failure(e) }

fun e7b() = runBlocking {
    val job = launch {
        repeat(3) { n -> log("  $n: " + runCatchingCancellable { fetch() }) }
        log("  ここは表示されない")
    }
    delay(150); job.cancelAndJoin(); log("  isCancelled=${job.isCancelled}")
}
== E7b
  0: Success(data)
  isCancelled=true

キャンセルの後、ループは止まりました。inline にしているので、block の中で suspend 関数を呼べます。ただし、この関数は (b) のように「withTimeout の時間切れを Failure で受けたい」場面でも投げ直してしまいます。その場面では withTimeoutOrNull を使うと、時間切れが null で返ります。

Result 型そのものの使い方は、関連記事「Kotlin Result型|runCatching・getOrElse・map一覧」で扱っています。

そのほかの動き

E8:複数の子が失敗したとき

fun e8() = runBlocking {
    val h = CoroutineExceptionHandler { _, e -> log("  handler: $e / suppressed=${e.suppressed.toList()}") }
    CoroutineScope(Job() + h).launch {
        launch { try { delay(Long.MAX_VALUE) } finally { throw ArithmeticException("後の例外") } }
        launch { delay(10); throw IOException("最初の例外") }
    }.join()
}
== E8
  handler: java.io.IOException: 最初の例外 / suppressed=[java.lang.ArithmeticException: 後の例外]

最初の例外が handler に届き、キャンセル中に起きた後の例外は suppressed(抑制された例外)に入りました。kotlinlang.org では「最初の例外が勝つ(the first exception wins)」と説明されています。ログでは e.suppressed も出すと、後の例外を見落としません。

E9:キャンセルは親に上がらない

fun e9() = runBlocking {
    val parent = launch {
        val child = launch { delay(Long.MAX_VALUE) }
        delay(10); child.cancel(); child.join()
        log("  子 isCancelled=${child.isCancelled}, 親 isActive=$isActive")
        launch { throw CancellationException("自分で投げた") }.join()
        log("  CancellationException を投げた子があっても親 isActive=$isActive")
    }
    parent.join()
}
== E9
  子 isCancelled=true, 親 isActive=true
  CancellationException を投げた子があっても親 isActive=true

子を cancel() しても、子が自分で CancellationException を投げても、親は動き続けました。ふつうの例外を CancellationException で包んで投げると、失敗が親に伝わらなくなるので、失敗を表すのには使いません。

E10:coroutineScope の外で try-catch する

suspend fun loadAll(): List<String> = coroutineScope {
    val a = async { delay(10); "A" }
    val b = async<String> { delay(20); throw IOException("B 失敗") }
    listOf(a.await(), b.await())
}

fun e10() = runBlocking {
    try { loadAll() } catch (e: IOException) { log("  coroutineScope の外で捕まえた: $e") }
    log("  runBlocking は続く(isActive=$isActive)")
}
== E10
  coroutineScope の外で捕まえた: java.io.IOException: B 失敗
  runBlocking は続く(isActive=true)

並列の処理をまとめて1つの失敗として扱うなら、この形が一番素直です。 coroutineScope は中の子が失敗すると、残りの子をキャンセルしてから、その例外を呼び出し元へ投げます。呼び出し元の try-catch で捕まえられ、外側のコルーチン(runBlocking)は失敗しませんでした。E2 との違いは、try-catch が await() ではなくスコープの外にあることです。

使い分けの判断表

場面 書き方
1つの処理の失敗に対応したい(再試行・代わりの値) その処理を try-catch(CancellationException は投げ直す)
並列の処理が全部そろって意味がある coroutineScope { async…; async… } を外から try-catch(E10)
並列の処理を独立させたい supervisorScope { } の中で launch/async し、各自で例外を処理(E2b・E3)
画面・サービスの寿命のスコープを作る CoroutineScope(SupervisorJob() + Dispatchers.… + handler)(E4 (e))
捕まえ損ねた例外をログ・通知に流す ルートのスコープに CoroutineExceptionHandler(E4 (a)(e))
suspend 関数を Result にしたい CancellationException だけ投げ直す関数で包む(E7b)

よくある質問

Q. CoroutineExceptionHandler で例外を受けたあと、処理を続けられますか。 A. 続けられません。KDoc では、handler が呼ばれた時点でコルーチンはもう例外で終わっている、とされています。続けたい処理は、例外が起きる場所の try-catch で書きます。handler はログ・通知・再起動のきっかけに使います。

Q. runBlocking に handler を付けても呼ばれません。 A. E4 (d) のとおり、runBlocking は子の例外を呼び出し元へ投げるので、handler は呼ばれませんでした。runBlocking の外を try-catch で囲みます。

Q. async に handler を付けても何も出ません。 A. async は例外を Deferred に入れて、await() で投げ直します(E4 (c))。await() しないと、例外はどこにも出ません。async を使ったら、必ず await() するか、coroutineScope の外で受けます。

Q. supervisorScope の孫(子の中の launch)も守られますか。 A. 守られるのは supervisorScope の直接の子だけです。子の中で launch した孫同士は、ふつうの親子関係なので、片方の失敗でもう片方はキャンセルされます(E5 と同じしくみ)。

Q. Flow の例外はどう扱いますか。 A. Flow では catch { } 演算子や、collect を囲む try-catch を使います。関連記事「Kotlin Flowの使い方 StateFlowとSharedFlowの違い」で扱っています。

まとめ

  • 子の失敗は親へ上がり、親は兄弟を止める。キャンセルは親へ上がらない
  • launch の例外は、launch を囲む try-catch では捕まらない(E1)。中で捕まえるか、coroutineScope の外で捕まえる
  • async の失敗は await() で受けられるが、親も失敗する(E2)。supervisorScope の中なら続けられる(E2b)
  • CoroutineExceptionHandler が効いたのはルートの launch だけ。子・async・runBlocking では効かなかった(E4)
  • アプリのスコープは SupervisorJob()。Job() だと1回の失敗でスコープごと終わった(E4 (f))
  • launch(SupervisorJob()) は中の兄弟を守らない(E5)
  • catch (e: Exception) と runCatching はキャンセルも捕まえる。CancellationException は投げ直す(E6・E7)

参考・出典

確認日はいずれも 2026年10月13日です。

  • Kotlin「Coroutine exceptions handling」:https://kotlinlang.org/docs/exception-handling.html
  • Kotlin「Cancellation and timeouts」:https://kotlinlang.org/docs/cancellation-and-timeouts.html
  • kotlinx.coroutines 1.8.0 のソース「CoroutineExceptionHandler.kt」:https://github.com/Kotlin/kotlinx.coroutines/blob/1.8.0/kotlinx-coroutines-core/common/src/CoroutineExceptionHandler.kt
  • kotlinx.coroutines API「SupervisorJob」:https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-supervisor-job.html
  • kotlinx.coroutines API「supervisorScope」:https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
  • Android Developers「Best practices for coroutines in Android」:https://developer.android.com/kotlin/coroutines/coroutines-best-practices

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlinコルーチン例外処理|SupervisorJobとHandlerの使い分け」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?