Penanganan Error pada Coroutines

CoroutineExceptionHandler dan semantik pembatalan.

Daftar isi

Error pada coroutine tidak pernah hilang tanpa jejak, melainkan menyebar mengikuti hierarki Job sebagaimana pembatalan menyebar. Topik ini membahas tiga jalur penanganan error yang lazim, yaitu blok try-catch di sekitar await, penyebaran kegagalan dari anak ke induk, dan CoroutineExceptionHandler pada coroutine akar, lalu menutup dengan kedudukan khusus CancellationException yang bukan merupakan kegagalan program.

Error pada async ditangkap di await#

Builder async tidak melempar error pada saat kejadiannya, melainkan menyimpannya di dalam Deferred dan melemparkannya kembali ketika await dipanggil. Penyimpanan itu menjadikan titik await sebagai tempat alami blok try-catch untuk menangkap error pekerjaan async, persis seperti menangkap error pemanggilan fungsi biasa.

kotlin tunda.kt
import kotlinx.coroutines.*

suspend fun muatData(): String {
    delay(100)
    throw IllegalStateException("Data tidak tersedia")
}

fun main() = runBlocking {
    supervisorScope {
        val tunda = async { muatData() }
        try {
            println("Data: ${tunda.await()}")
        } catch (e: IllegalStateException) {
            println("Ditangkap pada await: ${e.message}")
        }
    }
    println("Program berhenti secara normal")
}
bash
# compiler Kotlin dan library coroutines yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export LIBS=/home/kal/my-project/tools/libs
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi tunda.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc tunda.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d tunda.jar && java -cp "tunda.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" TundaKt
text Keluaran
Ditangkap pada await: Data tidak tersedia
Program berhenti secara normal

Error tertangkap penuh dan program berhenti dengan normal, tanpa jejak penyebaran apa pun. Pemakaian supervisorScope di sini sengaja dan berpengaruh. Pada scope biasa, kegagalan coroutine async juga segera disebar ke induknya sebelum await sempat dipanggil, sehingga blok try-catch di sekitar await saja tidak menahan seluruh penyebarannya dan scope yang diluncurkan dari runBlocking ikut batal. supervisorScope menghentikan penyebaran otomatis itu, sehingga await menjadi satu-satunya jalur keluar error dan tangkapan pada contoh berjalan bersih.

Kegagalan anak membatalkan induk#

Pada scope biasa, kegagalan satu anak membatalkan induknya, dan pembatalan induk menjalar ke seluruh anak lainnya. Mekanisme ini mencegah program melanjutkan pekerjaan yang bergantung pada hasil yang sudah rusak. Error anak yang tidak tertangkap punya dua sasaran sekaligus, yaitu menghentikan saudara-saudaranya dan naik ke pemanggil scope.

kotlin penyebaran.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        coroutineScope {
            launch {
                delay(100)
                throw IllegalStateException("Pembacaan sensor gagal")
            }
            launch {
                repeat(10) { langkah ->
                    println("Pencatat: langkah $langkah")
                    delay(150)
                }
            }
        }
    } catch (e: IllegalStateException) {
        println("Ditangkap pemanggil: ${e.message}")
    }
    println("Program berhenti secara normal")
}
bash
# kompilasi penyebaran.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc penyebaran.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d penyebaran.jar && java -cp "penyebaran.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" PenyebaranKt
text Keluaran
Pencatat: langkah 0
Ditangkap pemanggil: Pembacaan sensor gagal
Program berhenti secara normal

Coroutine pencatat direncanakan mencetak sepuluh langkah, tetapi hanya langkah 0 yang sempat dicetak. Ketika pembacaan sensor gagal pada milidetik ke-100, kegagalan itu membatalkan scope, dan pembatalan scope menghentikan pencatat pada delay berikutnya sebelum langkah 1 tercetak. Pemanggil coroutineScope menerima error aslinya melalui try-catch, sehingga program berhenti secara normal setelah penanganan selesai. Perhatikan pula bahwa pencatat berhenti tanpa mencetak pesan error apa pun, karena penghentiannya adalah CancellationException, bukan kegagalan.

CoroutineExceptionHandler pada coroutine akar#

Error yang mencapai puncak hierarki tanpa tertangkap siapa pun perlu penampung terakhir. CoroutineExceptionHandler adalah konteks tambahan yang dipasang pada scope atau pada coroutine akar untuk menampung error tersebut. Handler semacam ini hanya bekerja pada coroutine akar, yaitu coroutine yang diluncurkan langsung di dalam scope yang dibuat dengan CoroutineScope, karena coroutine anak menyerahkan errornya kepada induknya masing-masing.

kotlin handler.kt
import kotlinx.coroutines.*

val penangan = CoroutineExceptionHandler { _, e ->
    println("Ditangani handler: ${e.message}")
}

fun main() = runBlocking {
    val scope = CoroutineScope(Job() + penangan)
    val pekerjaan = scope.launch {
        throw IllegalStateException("Kejadian tidak terduga")
    }
    pekerjaan.join()
    println("Program tetap berjalan setelah kegagalan")
}
bash
# kompilasi handler.kt dengan library coroutines pada classpath, lalu jalankan programnya
kotlinc handler.kt -cp "$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar" -d handler.jar && java -cp "handler.jar:$LIBS/kotlinx-coroutines-core-jvm-1.11.0.jar:$STDLIB" HandlerKt
text Keluaran
Ditangani handler: Kejadian tidak terduga
Program tetap berjalan setelah kegagalan

Coroutine yang gagal diluncurkan langsung pada CoroutineScope(Job() + penangan), sehingga kegagalan tidak punya induk lain untuk dituju dan diterima handler. Program justru tetap berjalan normal setelahnya, dan join kembali tanpa melempar apa pun karena join tidak mengirimkan error, beda dengan await yang melemparnya. Pada runBlocking, kegagalan serupa justru muncul sebagai error di fungsi main, sebagaimana terlihat pada contoh penyebaran sebelumnya.

CancellationException bukan kegagalan#

CancellationException menempati kedudukan khusus di seluruh mekanisme ini. Jenis error itu adalah sinyal penghentian yang wajar, dipakai cancel() untuk menghentikan coroutine dan dipakai pembatalan induk untuk menghentikan anak, sehingga CoroutineExceptionHandler sengaja tidak menanganinya dan tidak diperlakukan sebagai kegagalan program.

Kedudukan khusus itu berdampak pada penulisan blok catch. Blok yang menangkap Exception secara luas akan ikut menangkap CancellationException karena jenisnya turunan dari Exception, dan menelannya berarti coroutine yang sedang dibatalkan menolak berhenti. Praktik yang benar adalah menangkap jenis error yang spesifik, dan apabila tangkapan luas memang diperlukan, CancellationException yang tertangkap dilempar kembali dengan throw sebelum penanganan lain dijalankan.

Ringkasan#

  • async menyimpan error di dalam Deferred dan melemparkannya pada await, sehingga try-catch di sekitar await adalah jalur penanganannya, idealnya di dalam supervisorScope agar penyebaran otomatis tidak ikut membatalkan scope.
  • Pada scope biasa, kegagalan satu anak membatalkan induk dan seluruh anak lain, lalu error aslinya naik ke pemanggil scope.
  • CoroutineExceptionHandler menampung error yang mencapai coroutine akar, dan tidak bekerja pada coroutine anak karena errornya diserahkan ke induk.
  • CancellationException adalah sinyal pembatalan yang wajar, tidak ditangani handler, dan tidak boleh ditelan oleh blok catch yang luas.

Referensi#