Exception adalah mekanisme penanganan kegagalan yang utama di JVM, namun tidak selalu paling cocok untuk semua situasi. Saat kegagalan adalah bagian normal dari alur bisnis (misal parse input pengguna yang tidak terprediksi), penggunaan exception bisa terasa berlebihan. Kotlin menyediakan tipe Result<T> dari standard library untuk merepresentasikan nilai sukses atau kegagalan secara eksplisit tanpa melempar exception. Pada chapter ini kita akan mempelajari konstruksi Result, operasi seperti map dan fold, serta pedoman kapan memakainya.

Apa Itu Result #

Result<out T> adalah value class yang membungkus salah satu dari dua hal: nilai sukses bertipe T, atau Throwable sebagai kegagalan. Konstruksi dilakukan lewat Result.success(value) atau Result.failure(throwable). Tipe ini bersifat immutable, aman dipassing antar function tanpa risiko mutasi.

kotlin Konstruksi dasar Result
val ok: Result<Int> = Result.success(42)
val gagal: Result<Int> = Result.failure(IllegalStateException("oops"))
println(ok.isSuccess)         // true
println(gagal.isFailure)      // true

Result menyediakan property isSuccess dan isFailure untuk pengecekan status, serta method seperti getOrNull() (mengembalikan T?), getOrDefault(...), getOrElse { ... }, dan exceptionOrNull() (mengembalikan Throwable?).

Mengonstruksi Result dari Exception #

Salah satu pola paling umum adalah membungkus operasi yang melempar exception ke Result. Function runCatching { ... } dari standard library melakukan hal ini otomatis: bila blok melempar Throwable, hasilnya Result.failure(e); bila tidak, hasilnya Result.success(value).

kotlin
// Demo kotlin.Result: success/failure, map, getOrNull, getOrElse, fold, recoverCatching.

fun parseAngka(teks: String): Result<Int> {
    return try {
        Result.success(teks.toInt())
    } catch (e: NumberFormatException) {
        Result.failure(e)
    }
}

fun bagi(a: Int, b: Int): Result<Int> {
    return if (b == 0) {
        Result.failure(ArithmeticException("pembagi nol"))
    } else {
        Result.success(a / b)
    }
}

fun main() {
    println("=== Result.success / Result.failure ===")
    val ok: Result<Int> = Result.success(42)
    val gagal: Result<Int> = Result.failure(IllegalStateException("oops"))
    println("ok.isSuccess=${ok.isSuccess}, ok.getOrNull()=${ok.getOrNull()}")
    println("gagal.isFailure=${gagal.isFailure}, gagal.exceptionOrNull()=${gagal.exceptionOrNull()}")

    println()
    println("=== parseAngka dengan Result ===")
    for (t in listOf("7", "abc", "")) {
        val r = parseAngka(t)
        println("parseAngka(\"$t\") -> isSuccess=${r.isSuccess}, " +
            "getOrNull=${r.getOrNull()}, " +
            "getOrElse=${r.getOrElse { -1 }}")
    }

    println()
    println("=== map untuk transformasi ===")
    val parsed = parseAngka("5")        // Result<Int>
    val doubled = parsed.map { it * 2 }  // Result<Int> hasilnya 10
    val failedMap = parseAngka("abc").map { it * 2 }  // tetap Failure
    println("doubled = $doubled")
    println("failedMap.isSuccess = ${failedMap.isSuccess}, exceptionOrNull=${failedMap.exceptionOrNull()}")

    println()
    println("=== fold untuk menangani kedua cabang ===")
    val hasil = parseAngka("abc").fold(
        onSuccess = { "nilai=$it" },
        onFailure = { err -> "error: ${err::class.simpleName}" }
    )
    println("fold hasil: $hasil")

    println()
    println("=== getOrElse untuk default ===")
    val nilai1: Int = parseAngka("abc").getOrElse { -1 }
    val nilai2: Int = parseAngka("99").getOrElse { -1 }
    println("getOrElse abc = $nilai1, getOrElse 99 = $nilai2")

    println()
    println("=== recoverCatching untuk fallback ===")
    val dipulihkan: Result<Int> = parseAngka("xyz").recoverCatching {
        // menjalankan blok bila failure, mengembalikan nilai sukses baru
        0
    }
    println("recoverCatching = $dipulihkan")

    println()
    println("=== Pipeline map berantai ===")
    val pipeline: Result<String> = parseAngka("10")
        .map { it + 5 }       // 15
        .map { it * 2 }       // 30
        .map { "hasil=$it" }
    println("pipeline = $pipeline")

    println()
    println("=== Catatan: Result tidak boleh dipakai sebagai return type public API ===")
    println("Stdlib menyarankan Result untuk kode internal; " +
        "untuk API publik, gunakan sealed class domain.")
}

Output yang Dihasilkan:

text
=== Result.success / Result.failure ===
ok.isSuccess=true, ok.getOrNull()=42
gagal.isFailure=true, gagal.exceptionOrNull()=java.lang.IllegalStateException: oops

=== parseAngka dengan Result ===
parseAngka("7") -> isSuccess=true, getOrNull=7, getOrElse=7
parseAngka("abc") -> isSuccess=false, getOrNull=null, getOrElse=-1
parseAngka("") -> isSuccess=false, getOrNull=null, getOrElse=-1

=== map untuk transformasi ===
doubled = Success(10)
failedMap.isSuccess = false, exceptionOrNull=java.lang.NumberFormatException: For input string: "abc"

=== fold untuk menangani kedua cabang ===
fold hasil: error: NumberFormatException

=== getOrElse untuk default ===
getOrElse abc = -1, getOrElse 99 = 99

=== recoverCatching untuk fallback ===
recoverCatching = Success(0)

=== Pipeline map berantai ===
pipeline = Success(hasil=30)

=== Catatan: Result tidak boleh dipakai sebagai return type public API ===
Stdlib menyarankan Result untuk kode internal; untuk API publik, gunakan sealed class domain.

Mari kita bedah bagian-bagian penting dari kode di atas:

  1. parseAngka: Membungkus teks.toInt() dalam try-catch dan mengembalikan Result.success atau Result.failure. Pemanggil mendapat tanda eksplisit bahwa function ini bisa gagal.
  2. bagi: Mengembalikan Result<Int> dengan eksplisit branching if (b == 0) tanpa melempar.
  3. map { it * 2 }: Mengubah nilai sukses tanpa menyentuh Throwable. Bila Result adalah Failure, map dilewat dan Throwable dibawa sampai akhir pipeline.
  4. fold(onSuccess, onFailure): Pola akhir untuk mengonsumsi Result. Kedua cabang harus mengembalikan tipe yang sama.
  5. getOrElse { -1 }: Mengembalikan nilai default bila Result failure. Lambda menerima Throwable sebagai parameter.
  6. recoverCatching { 0 }: Bila Result failure, lambda dijalankan dan hasilnya dibungkus sebagai Success.
  7. Pipeline map berantai: Menggabungkan beberapa transformasi. Bila salah satu langkah gagal, sisanya dilewatkan dan failure awal dibawa sampai akhir.

Operasi Penting pada Result #

Berikut ringkasan operasi yang paling sering dipakai:

OperasiTipe kembalianKeterangan
getOrNull()T?Nilai sukses atau null bila failure
getOrDefault(d)TNilai sukses atau d
getOrElse { e -> ... }TNilai sukses atau hasil lambda
exceptionOrNull()Throwable?Exception bila failure, null bila sukses
map { ... }Result<R>Transformasi nilai sukses
mapCatching { ... }Result<R>Seperti map, tapi bungkus exception ke Failure
recoverCatching { e -> ... }Result<T>Pulihkan dari failure ke nilai baru
fold(onSuccess, onFailure)RKonsumsi kedua cabang dengan tipe yang sama
onSuccess { ... } / onFailure { ... }Result<T>Side-effect, lalu kembalikan Result

Kapan Memakai Result #

Result cocok untuk situasi berikut: parse input pengguna, lookup key di map, atau validasi format di mana kegagalan adalah hasil normal. Result cocok juga untuk pipeline transformasi di mana setiap langkah bisa gagal dan kesalahan merambat deklaratif lewat map, serta kode internal di mana kita ingin tipe yang menandai kemungkinan kegagalan tanpa melempar.

Result tidak cocok untuk kondisi yang tidak terduga / bug (misal null yang seharusnya tidak terjadi atau state internal rusak); gunakan throw IllegalStateException agar stack trace terekam. Result juga tidak cocok untuk API publik library karena Result adalah value class dengan instance Failure yang membawa Throwable.

Result vs Exception #

Pemilihan Result atau exception bergantung pada sifat kegagalan:

KarakteristikPakai ExceptionPakai Result
Frekuensi kegagalanJarang (penyimpangan)Sering (alur normal)
Penanganan idealTangkap di layer atasPipa lewat map
Biaya performaLebih mahal (stack trace)Lebih ringan
KomposisiManual dengan tryDeklaratif dengan map/fold
Konteks kegagalanThrowable + pesanThrowable atau nilai domain

Untuk operasi seperti parse input pengguna yang sering gagal, Result memberi komposisi yang lebih bersih. Untuk kegagalan yang jarang terjadi dan butuh stack trace penuh (misal koneksi database terputus), exception tetap pilihan yang tepat.

Referensi Lanjutan #

  • Result API reference — API reference tipe Result<T> beserta map, fold, recoverCatching
  • Exceptions — konteks kapan memilih exception dibanding Result

Penutup #

Tipe Result<T> memberi alternatif deklaratif untuk merepresentasikan kegagalan yang merupakan bagian normal dari alur program. Dengan map, fold, dan recoverCatching, kita membangun pipeline transformasi yang menangani kedua cabang secara eksplisit. Untuk kegagalan yang menggambarkan bug atau kondisi darurat, exception tetap pilihan utama. Berikutnya, kita membedakan dua kategori kegagalan yang sering tertukar: Error versus Exception.