Operasi asynchronous kadang macet — server tidak merespons, database terlalu lambat, atau deadlock tersembunyi. Tanpa timeout, coroutine bisa menggantung selamanya, menghabiskan thread pool dan merusak pengalaman pengguna. Kotlin menyediakan dua builder untuk ini: withTimeout (melempar exception) dan withTimeoutOrNull (mengembalikan null). Bab ini membongkar keduanya beserta perilaku pembatalan otomatis yang dipicu timeout.
withTimeout #
withTimeout(duration) { ... } adalah suspend function yang menjalankan blok dengan batas waktu. Jika blok tidak selesai sebelum waktu habis, TimeoutCancellationException (subClass CancellationException) dilemparkan.
import kotlinx.coroutines.*
fun main() = runBlocking {
// 1. withTimeout: lempar TimeoutCancellationException
println("=== 1. withTimeout ===")
try {
withTimeout(80) {
repeat(5) { i ->
println("[tugas] langkah-$i")
delay(30)
}
}
} catch (e: TimeoutCancellationException) {
println("[1] timeout terjadi: ${e::class.simpleName}")
}
// 2. withTimeoutOrNull: kembalikan null saat timeout
println()
println("=== 2. withTimeoutOrNull ===")
val hasil: String? = withTimeoutOrNull(80) {
repeat(5) { i ->
println("[tugas] langkah-$i")
delay(30)
}
"sukses"
}
println("[2] hasil=$hasil")
// 3. Operasi cepat selesai sebelum timeout
println()
println("=== 3. selesai sebelum timeout ===")
val cepat: String? = withTimeoutOrNull(100) {
delay(30)
"cepat"
}
println("[3] hasil=$cepat")
}Output yang Dihasilkan:
=== 1. withTimeout ===
[tugas] langkah-0
[tugas] langkah-1
[tugas] langkah-2
[1] timeout terjadi: TimeoutCancellationException
=== 2. withTimeoutOrNull ===
[tugas] langkah-0
[tugas] langkah-1
[tugas] langkah-2
[2] hasil=null
=== 3. selesai sebelum timeout ===
[3] hasil=cepatMari kita telaah baris-baris kode di atas:
- Baris 7-15 (
withTimeout(80) { repeat(5) { ... delay(30) } }): blok dijalankan dengan batas 80ms. Lima iterasi masing-masing 30ms = 150ms total — melebihi batas. Timeout terjadi setelah iterasi ke-2 (sekitar 90ms). ExceptionTimeoutCancellationExceptiondilemparkan, ditangkap dicatch. - Baris 19-28 (
withTimeoutOrNull(80) { ... "sukses" }): varian yang tidak melempar. Jika timeout, hasil adalahnull; jika sukses, hasil adalah nilai blok. Outputhasil=nullmenunjukkan timeout terjadi sebelum blok mencapai barisreturn "sukses". - Baris 32-37 (
withTimeoutOrNull(100) { delay(30); "cepat" }): blok selesai dalam 30ms, jauh sebelum batas 100ms. Hasilnya"cepat"— bukannull. Inilah pola “best-effort”: coba lakukan, kembalikan default bila gagal. - Output yang terlihat: pada blok 1 dan 2,
langkah-0,langkah-1,langkah-2tercetak lalu berhenti — bukanlangkah-4. Hal ini karenadelay(30)di iterasi ke-3 (sekitar 90ms) sudah melewati batas 80ms. Coroutine dibatalkan kooperatif saatdelaymengecek status.
Cara Kerja Timeout #
withTimeout menambahkan sebuah Job anak dengan timer. Saat timer habis:
- Runtime membatalkan job anak — menandai sebagai cancelled.
- Cancellation point berikutnya (mis.
delay,await) melemparCancellationException. - Jika blok tidak menangkap, exception merambat ke pemanggil.
Karena pembatalan bersifat kooperatif, kode CPU-bound tanpa cancellation point tidak akan merespons timeout. Sama seperti cancel() pada Job, sisipkan ensureActive() di loop panjang.
try {
withTimeout(100) { delay(200) }
} catch (e: TimeoutCancellationException) {
println("timeout spesifik")
} catch (e: CancellationException) {
println("pembatalan biasa: ${e.message}")
}withTimeoutOrNull untuk Fallback #
withTimeoutOrNull cocok untuk operasi “best-effort” di mana kegagalan sebaiknya kembali ke default, bukan melempar exception. Pola umum:
suspend fun ambilDenganFallback(kunci: String): Data {
return withTimeoutOrNull(500) {
cache.get(kunci) // bisa lambat jika cache sibuk
} ?: ambilDariDatabase(kunci) // fallback jika timeout
}Pembersihan Resource saat Timeout #
Karena timeout membatalkan coroutine lewat CancellationException, blok finally tetap berjalan. Manfaatkan ini untuk pembersihan resource:
withTimeout(1000) {
val koneksi = bukaKoneksi()
try {
koneksi.kirim(request)
} finally {
// tetap berjalan meski timeout
koneksi.tutup()
}
}withTimeout(1000) {
try {
lakukanOperasi()
} catch (e: Exception) {
// cleanup suspend di NonCancellable scope
withContext(NonCancellable) {
koneksi.tutupAsync()
}
}
}Referensi Lanjutan #
- Coroutine basics
— dasar
withTimeoutdanwithTimeoutOrNull - Composing suspending functions
— pola timeout dalam pemanggilan
suspendfunction
Selanjutnya #
Dengan timeout di tangan, kita siap membahas async dan await secara mendalam — bukan sekedar pengenalan, tetapi pola untuk konkurensi paralel dengan jumlah variabel. Bab async dan await akan memperlihatkan idiom awaitAll, pattern concurrent fetch, dan jebakan async yang tidak ditunggu.