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 #
| Mekanisme | Kapan dipakai | Efek |
|---|---|---|
try/catch di dalam blok coroutine | Exception diprediksi dan ditangani lokal | Tidak ada propagasi; coroutine tetap berjalan |
CoroutineExceptionHandler | Exception tidak terduga di root coroutine | Dicatat; coroutine tersebut gagal |
SupervisorJob | Anak tidak boleh saling membatalkan | Kegagalan satu anak tidak membatalkan saudara |
Aturan dasar penanganan exception di coroutine:
try/catchbiasa tetap berfungsi — gunakan untuk exception yang diprediksi (mis. parsing input, validasi).- Exception yang lepas dari sebuah
launchakan membatalkan saudara dan merambat ke parent. - Exception yang lepas dari
asynchanya muncul saatawait()dipanggil. CoroutineExceptionHandlerhanya efektif di root coroutine (anak langsung dari scope dengan SupervisorJob).SupervisorJobmengisolasi kegagalan antar anak — saudara tetap berjalan.
Demonstrasi Mekanisme #
Kode berikut mendemonstrasikan dua mekanisme utama: try/catch lokal dan SupervisorJob + CoroutineExceptionHandler:
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:
=== 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] selesaiMari kita telaah baris-baris kode di atas:
- Baris 3-5 (
val handler = CoroutineExceptionHandler { ... }): handler kustom yang dicatat saat exception tidak tertangkap di root coroutine. Handler menerimaCoroutineContextdanThrowable— kita bisa membacaCoroutineNameuntuk identifikasi coroutine mana yang gagal. - Baris 10-19 (
launch { try { ... } catch (e) { ... } }): pola standartry/catch. Exception ditangkap di dalam coroutine, tidak merambat. Coroutine selesai normal (status completed, bukan cancelled). - Baris 24 (
CoroutineScope(SupervisorJob() + handler + ...)): membuat scope kustom denganSupervisorJobsebagai penggantiJobreguler.SupervisorJobmengubah perilaku propagasi: kegagalan satu anak tidak membatalkan saudara. Handler ditambahkan ke konteks sehingga exception yang lepas dari anak diproses handler, bukan dilempar ke parent. - Baris 25-29 (
scope.launch(CoroutineName("anak-1")) { throw RuntimeException(...) }): anak pertama sengaja melempar exception setelah 30ms. Karena parent adalahSupervisorJob, exception tidak membatalkan anak-2. Handler dipanggil dengan konteks anak-1. - 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. - Baris 36 (
scope.cancel()): membersihkan scope setelah kedua anak selesai diproses. Tanpa ini, scope akan terus hidup karenaSupervisorJobtidak 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.
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:
| Aspek | Job reguler | SupervisorJob |
|---|---|---|
| Kegagalan anak | Membatalkan semua saudara, lalu merambat ke parent | Tidak membatalkan saudara; exception diteruskan ke handler |
| Pemakaian umum | coroutineScope, launch reguler | supervisorScope, scope kustom untuk service yang mengisolasi kegagalan |
| Propagasi exception | Ke parent scope | Ke handler (jika ada) atau ke parent scope |
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:
- Catch
CancellationException: jangan menelan exception ini. Jika harus catch untuk cleanup, lempar ulang setelahnya. - Handler di anak: tidak efektif. Pasang handler di root scope.
asynctanpaawait: exception bisa bocor ke parent. Selaluawait()atau tangkap secara eksplisit.SupervisorJobdilaunchanak: tidak efektif karenalaunchselalu membuatJobbaru. GunakanSupervisorJobdi scope, bukan dilaunch.
Referensi Lanjutan #
- Coroutine context and dispatchers
— panduan
CoroutineExceptionHandlerdan propagasi exception - Composing suspending functions
— perilaku exception pada pemanggilan
suspendfunction
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.