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.

kotlin
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:

text
=== 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=cepat

Mari kita telaah baris-baris kode di atas:

  1. 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). Exception TimeoutCancellationException dilemparkan, ditangkap di catch.
  2. Baris 19-28 (withTimeoutOrNull(80) { ... "sukses" }): varian yang tidak melempar. Jika timeout, hasil adalah null; jika sukses, hasil adalah nilai blok. Output hasil=null menunjukkan timeout terjadi sebelum blok mencapai baris return "sukses".
  3. Baris 32-37 (withTimeoutOrNull(100) { delay(30); "cepat" }): blok selesai dalam 30ms, jauh sebelum batas 100ms. Hasilnya "cepat" — bukan null. Inilah pola “best-effort”: coba lakukan, kembalikan default bila gagal.
  4. Output yang terlihat: pada blok 1 dan 2, langkah-0, langkah-1, langkah-2 tercetak lalu berhenti — bukan langkah-4. Hal ini karena delay(30) di iterasi ke-3 (sekitar 90ms) sudah melewati batas 80ms. Coroutine dibatalkan kooperatif saat delay mengecek status.

Cara Kerja Timeout #

withTimeout menambahkan sebuah Job anak dengan timer. Saat timer habis:

  1. Runtime membatalkan job anak — menandai sebagai cancelled.
  2. Cancellation point berikutnya (mis. delay, await) melempar CancellationException.
  3. 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.

kotlin Contoh: bedakan timeout dan pembatalan biasa
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:

kotlin Contoh: cache dengan timeout
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:

kotlin Contoh: tutup koneksi di finally
withTimeout(1000) {
    val koneksi = bukaKoneksi()
    try {
        koneksi.kirim(request)
    } finally {
        // tetap berjalan meski timeout
        koneksi.tutup()
    }
}
kotlin Pola: suspend di dalam finally setelah timeout
withTimeout(1000) {
    try {
        lakukanOperasi()
    } catch (e: Exception) {
        // cleanup suspend di NonCancellable scope
        withContext(NonCancellable) {
            koneksi.tutupAsync()
        }
    }
}

Referensi Lanjutan #

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.