Penanganan exception pada coroutines memiliki beberapa perbedaan penting dari kode synchronous biasa. Sebuah exception yang tidak tertangkap di dalam coroutine tidak hanya menghentikan coroutine itu sendiri — ia juga membatalkan saudara dan merambat ke induk. Bab ini membahas tiga mekanisme penanganan: try/catch lokal di dalam coroutine, CoroutineExceptionHandler untuk exception yang tidak tertangkap, dan SupervisorJob untuk mengisolasi kegagalan antar coroutine anak. Pemilihan mekanisme yang tepat menentukan apakah sistem tetap berjalan saat sebagian komponen gagal.

Tiga Mekanisme Penanganan #

MekanismeKapan dipakaiEfek
try/catch di dalam blok coroutineException diprediksi dan ditangani lokalTidak ada propagasi; coroutine tetap berjalan
CoroutineExceptionHandlerException tidak terduga di root coroutineDicatat; coroutine tersebut gagal
SupervisorJobAnak tidak boleh saling membatalkanKegagalan satu anak tidak membatalkan saudara

Aturan dasar penanganan exception di coroutine:

  1. try/catch biasa tetap berfungsi — gunakan untuk exception yang diprediksi (mis. parsing input, validasi).
  2. Exception yang lepas dari sebuah launch akan membatalkan saudara dan merambat ke parent.
  3. Exception yang lepas dari async hanya muncul saat await() dipanggil.
  4. CoroutineExceptionHandler hanya efektif di root coroutine (anak langsung dari scope dengan SupervisorJob).
  5. SupervisorJob mengisolasi kegagalan antar anak — saudara tetap berjalan.

Demonstrasi Mekanisme #

Kode berikut mendemonstrasikan dua mekanisme utama: try/catch lokal dan SupervisorJob + CoroutineExceptionHandler:

kotlin
import kotlinx.coroutines.*

val handler = CoroutineExceptionHandler { ctx, e ->
    println("[handler] coroutine=${ctx[CoroutineName]?.name} exception=${e::class.simpleName} msg=${e.message}")
}

fun main() = runBlocking {
    // 1. try-catch di dalam coroutine: exception ditangkap lokal
    println("=== 1. try-catch di dalam coroutine ===")
    launch {
        try {
            delay(30)
            throw IllegalArgumentException("argumen tidak valid")
        } catch (e: IllegalArgumentException) {
            println("[1] tangkap: ${e.message}")
        }
    }.join()

    // 2. CoroutineScope dengan SupervisorJob + handler: anak gagal, saudara tetap jalan
    println()
    println("=== 2. SupervisorJob + handler (isolasi kegagalan) ===")
    val scope = CoroutineScope(SupervisorJob() + handler + CoroutineName("root-supervisor"))
    scope.launch(CoroutineName("anak-1")) {
        delay(30)
        throw RuntimeException("anak-1 gagal")
    }
    scope.launch(CoroutineName("anak-2")) {
        delay(60)
        println("[anak-2] tetap selesai, tidak dibatalkan")
    }
    delay(120)
    scope.cancel()
    println("[2] selesai")
}

Output yang Dihasilkan:

text
=== 1. try-catch di dalam coroutine ===
[1] tangkap: argumen tidak valid

=== 2. SupervisorJob + handler (isolasi kegagalan) ===
[anak-2] tetap selesai, tidak dibatalkan
[handler] coroutine=anak-1 exception=RuntimeException msg=anak-1 gagal
[2] selesai

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-5 (val handler = CoroutineExceptionHandler { ... }): handler kustom yang dicatat saat exception tidak tertangkap di root coroutine. Handler menerima CoroutineContext dan Throwable — kita bisa membaca CoroutineName untuk identifikasi coroutine mana yang gagal.
  2. Baris 10-19 (launch { try { ... } catch (e) { ... } }): pola standar try/catch. Exception ditangkap di dalam coroutine, tidak merambat. Coroutine selesai normal (status completed, bukan cancelled).
  3. Baris 24 (CoroutineScope(SupervisorJob() + handler + ...)): membuat scope kustom dengan SupervisorJob sebagai pengganti Job reguler. SupervisorJob mengubah perilaku propagasi: kegagalan satu anak tidak membatalkan saudara. Handler ditambahkan ke konteks sehingga exception yang lepas dari anak diproses handler, bukan dilempar ke parent.
  4. Baris 25-29 (scope.launch(CoroutineName("anak-1")) { throw RuntimeException(...) }): anak pertama sengaja melempar exception setelah 30ms. Karena parent adalah SupervisorJob, exception tidak membatalkan anak-2. Handler dipanggil dengan konteks anak-1.
  5. Baris 30-34 (scope.launch(CoroutineName("anak-2")) { ... }): anak kedua tetap berjalan hingga selesai pada 60ms, meskipun anak-1 telah gagal pada 30ms. Inilah inti isolasi kegagalan.
  6. Baris 36 (scope.cancel()): membersihkan scope setelah kedua anak selesai diproses. Tanpa ini, scope akan terus hidup karena SupervisorJob tidak otomatis selesai saat anak gagal.

Output menunjukkan pola yang khas: anak-2 menyelesaikan eksekusinya, kemudian handler mencatat exception dari anak-1. Tidak ada pembatalan saudara.

CoroutineExceptionHandler #

CoroutineExceptionHandler adalah elemen konteks yang dipanggil saat exception tidak tertangkap di sebuah coroutine. Pemakaian umum: logging terpusat, metrik error, fallback behavior.

kotlin Contoh: handler untuk logging
val logHandler = CoroutineExceptionHandler { ctx, e ->
    val nama = ctx[CoroutineName]?.name ?: "anonim"
    logger.error("coroutine=$nama gagal: ${e.message}", e)
}

// Pasang di root scope
val scope = CoroutineScope(SupervisorJob() + logHandler)

SupervisorJob vs Job #

Perbedaan utama SupervisorJob dan Job:

AspekJob regulerSupervisorJob
Kegagalan anakMembatalkan semua saudara, lalu merambat ke parentTidak membatalkan saudara; exception diteruskan ke handler
Pemakaian umumcoroutineScope, launch regulersupervisorScope, scope kustom untuk service yang mengisolasi kegagalan
Propagasi exceptionKe parent scopeKe handler (jika ada) atau ke parent scope
kotlin Pola umum: service dengan SupervisorJob
class UserService(
    private val repo: UserRepository,
    private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + logHandler),
) {
    fun muatSemua() = scope.launch {
        // kegagalan di sini tidak membatalkan operasi lain
        repo.findAll()
    }
}

Jebakan Umum #

Beberapa jebakan yang sering dijumpai:

  1. Catch CancellationException: jangan menelan exception ini. Jika harus catch untuk cleanup, lempar ulang setelahnya.
  2. Handler di anak: tidak efektif. Pasang handler di root scope.
  3. async tanpa await: exception bisa bocor ke parent. Selalu await() atau tangkap secara eksplisit.
  4. SupervisorJob di launch anak: tidak efektif karena launch selalu membuat Job baru. Gunakan SupervisorJob di scope, bukan di launch.

Referensi Lanjutan #

Selanjutnya #

Dengan memahami penanganan exception, kita siap membahas withTimeout — cara membatalkan coroutine secara otomatis setelah waktu tertentu. Bab Coroutine Timeouts akan memperlihatkan withTimeout, withTimeoutOrNull, dan pola pembatalan otomatis untuk operasi yang tidak boleh menggantung.