Pipeline data asinkron harus tangguh terhadap kegagalan: network timeout, data korup, atau dependency tidak tersedia. Bagian 3 membahas empat pola error handling yang umum dipakai di produksi: SupervisorJob untuk isolasi, Flow.catch untuk streaming, retryWhen untuk retry dengan kondisi, dan Result<T> untuk expected error.

Empat Pola Error Handling #

PolaTujuanAPI Kotlin
SupervisorJobAnak yang gagal tidak membatalkan saudaraCoroutineScope(SupervisorJob())
Flow.catchTangkap error di tengah stream, emit fallbackflow.catch { e -> emit(...) }
retryWhenRetry dengan kondisi (mis. hanya transient error)Custom suspend function
Result<T>Expected error tanpa exceptionrunCatching { }, .map, .getOrElse

Implementasi Lengkap #

Berikut program yang mendemonstrasikan keempat pola:

kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

// === Error handling dengan SupervisorJob ===
// Anak yang gagal tidak membatalkan saudaranya.

data class JobResult(val name: String, val status: String, val error: String? = null)

suspend fun prosesStage(nama: String, shouldFail: Boolean): JobResult {
    kotlinx.coroutines.delay(20)
    return if (shouldFail) {
        JobResult(nama, "gagal", "RuntimeException pada $nama")
    } else {
        JobResult(nama, "sukses")
    }
}

// === retryWhen: retry berdasarkan kondisi ===
suspend fun <T> retryWhen(
    maxAttempts: Int = 5,
    shouldRetry: (Throwable, attempt: Int) -> Boolean,
    block: suspend (attempt: Int) -> T
): T {
    var attempt = 0
    while (true) {
        attempt++
        try {
            return block(attempt)
        } catch (e: Throwable) {
            if (attempt >= maxAttempts || !shouldRetry(e, attempt)) {
                throw e
            }
            println("  [retry] attempt $attempt gagal: ${e.message}")
            kotlinx.coroutines.delay(50L * attempt) // linear backoff
        }
    }
}

fun main() = runBlocking {
    println("=== SupervisorJob: isolasi failure ===")
    val results = mutableListOf<JobResult>()

    val supervisor = CoroutineScope(SupervisorJob())
    val jobs = listOf("fetch-a" to false, "fetch-b" to true, "fetch-c" to false).map { (name, fail) ->
        supervisor.async { prosesStage(name, fail) }
    }

    // awaitAll dengan try-catch per job (SupervisorJob tidak propagate failure)
    jobs.forEach { job ->
        try {
            results.add(job.await())
        } catch (e: Throwable) {
            results.add(JobResult("unknown", "exception", e.message))
        }
    }

    results.forEach { println("  $it") }

    println("\n=== Flow.catch: tangani error di tengah stream ===")
    val flow = flow {
        emit(1)
        emit(2)
        throw RuntimeException("error di tengah")
    }

    flow.catch { e ->
        println("  [catch] error: ${e.message}")
        emit(-1) // emit fallback
    }.collect { v ->
        println("  emit: $v")
    }

    println("\n=== retryWhen: hanya retry untuk transient error ===")
    var attemptCount = 0
    val hasil = retryWhen(
        maxAttempts = 5,
        shouldRetry = { e, attempt -> e is RuntimeException && attempt < 3 }
    ) { attempt ->
        println("  [attempt $attempt]")
        attemptCount = attempt
        if (attempt < 2) throw RuntimeException("transient")
        "OK pada attempt $attempt"
    }
    println("Hasil: $hasil (total attempts: $attemptCount)")

    println("\n=== Pola Result<T>: tidak pakai exception untuk control flow ===")
    val r: Result<String> = runCatching {
        if (attemptCount < 0) throw IllegalStateException("tidak mungkin")
        "sukses"
    }
    println("Result: $r")
    println("Map: ${r.map { it.uppercase() }}")
    println("GetOrNull: ${r.getOrNull()}")
    println("getOrElse: ${r.getOrElse { "default" }}")

    println("\nError handling pipeline selesai.")
}

Output yang Dihasilkan:

text
=== SupervisorJob: isolasi failure ===
  JobResult(name=fetch-a, status=sukses, error=null)
  JobResult(name=fetch-b, status=gagal, error=RuntimeException pada fetch-b)
  JobResult(name=fetch-c, status=sukses, error=null)

=== Flow.catch: tangani error di tengah stream ===
  emit: 1
  emit: 2
  [catch] error: error di tengah
  emit: -1

=== retryWhen: hanya retry untuk transient error ===
  [attempt 1]
  [retry] attempt 1 gagal: transient
  [attempt 2]
Hasil: OK pada attempt 2 (total attempts: 2)

=== Pola Result<T>: tidak pakai exception untuk control flow ===
Result: Success(sukses)
Map: Success(SUKSES)
GetOrNull: sukses
getOrElse: sukses

Error handling pipeline selesai.

Mari kita bedah empat pola di atas:

  1. CoroutineScope(SupervisorJob()): scope di mana child failure tidak propagate ke sibling. Tanpa SupervisorJob, satu async yang gagal akan membatalkan semua sibling. Cocok untuk pipeline yang menerima kegagalan parsial.
  2. flow.catch { e -> emit(-1) }: operator catch pada Flow menangkap exception upstream dan mengubahnya menjadi emit downstream. Alternatif untuk fallback value tanpa menghentikan stream.
  3. retryWhen { e, attempt -> ... }: custom suspend function yang menerima predicate. Predicate menentukan apakah error tertentu layak retry. Contoh: retry hanya untuk IOException (transient), langsung gagal untuk IllegalArgumentException (programmer error).
  4. runCatching { }.map { }: Result monad — bungkus hasil dalam container yang menampung baik value maupun error. Cocok untuk expected error (mis. parsing input). Berbeda dengan exception, Result tidak menggangu control flow.

Best Practice #

  • Transient vs permanent error: retry hanya untuk transient (network, timeout). Jangan retry permanent (validation error, 4xx HTTP).
  • Backoff: selalu gunakan delay antara retry untuk menghindari overwhelming downstream. Eksponensial lebih disukai.
  • Circuit breaker: jika banyak retry gagal berturut-turut, hentikan mencoba untuk sementara (cooldown). Library resilience4j menyediakan ini.
  • Dead letter queue: untuk pipeline message, simpan pesan yang gagal ke queue terpisah untuk investigasi manual.
  • Observability: log setiap retry, error, dan fallback. Metrik penting: retry_count, error_rate, p99_latency.

Referensi Lanjutan #

Selanjutnya #

Pada Bagian 4 (terakhir), kita akan menulis pengujian untuk pipeline asinkron ini dan mengukur performanya dengan metrik throughput dan latency.