Setelah mempelajari mekanisme exception di Kotlin, kita perlu merangkainya menjadi strategi penanganan kegagalan yang sehat. Bukan hanya tentang menulis try-catch, tetapi tentang kapan melempar, di mana menangkap, dan bagaimana memilih antara exception dan Result. Pada chapter ini kita akan merangkum tujuh praktik terbaik yang akan membuat kode lebih mudah dipulihkan dan diperiksa. Setiap praktik disertai contoh kode konkret yang dapat dijalankan.
1. Fail Fast #
Fail fast berarti memvalidasi argumen dan state sedini mungkin, sebelum menjalankan logika berat. Dengan menolak input tidak valid di awal, kita menghindari eksekusi parsial yang meninggalkan state tidak konsisten. Kotlin menyediakan helper require dan check untuk pola ini.
fun prosesPesanan(idPelanggan: Int, sku: String, qty: Int): String {
require(idPelanggan > 0) { "idPelanggan harus positif: $idPelanggan" }
require(qty > 0) { "qty harus positif: $qty" }
require(sku.isNotBlank()) { "sku tidak boleh kosong" }
// ... logika utama setelah argumen dipastikan valid
}Tanpa fail fast, kode akan menjalankan beberapa langkah sebelum akhirnya gagal di tengah. Pesan exception pun menjadi tidak jelas akar masalahnya, dan side-effect parsial sulit dipulihkan.
Tujuh Praktik Terbaik dalam Satu Contoh #
Contoh berikut menyatukan tujuh praktik terbaik dalam satu file: fail fast, tipe exception spesifik, tidak menelan exception, log dengan konteks, lempar di layer yang tepat, gunakan Result untuk expected errors, dan pasang global handler di boundary program.
// Demo best practices: fail fast, no swallow, log with context, layer-appropriate,
// specific types, prefer Result for expected errors.
import java.io.IOException
import java.util.logging.Logger
// 1. Custom exception spesifik (lebih informatif daripada Exception generik).
class StokHabis(val sku: String, val diminta: Int, val tersedia: Int) :
RuntimeException("stok SKU $sku habis: butuh $diminta, tersedia $tersedia")
class PelangganTidakDitemukan(val idPelanggan: Int) :
RuntimeException("pelanggan id=$idPelanggan tidak ditemukan")
// 2. Fail fast: validasi argumen SEBELUM menjalankan logika berat.
fun prosesPesanan(idPelanggan: Int, sku: String, qty: Int): String {
// Validasi argumen lebih dulu — fail fast
require(idPelanggan > 0) { "idPelanggan harus positif: $idPelanggan" }
require(qty > 0) { "qty harus positif: $qty" }
require(sku.isNotBlank()) { "sku tidak boleh kosong" }
// Setelah validasi, jalankan logika dengan asumsi argumen valid.
val tersedia = 5
if (qty > tersedia) {
// Tipe spesifik + field konteks
throw StokHabis(sku, qty, tersedia)
}
return "pesanan $sku x $qty untuk pelanggan $idPelanggan diproses"
}
// 3. Jangan menelan exception diam-diam. Selalu log atau lempar ulang.
private val log = Logger.getLogger("BestPractices")!!
fun bacaKonfigurasi(path: String): String {
return try {
// simulasi: file tidak ditemukan
throw IOException("file tidak ada: $path")
} catch (e: IOException) {
// Log dengan konteks (path, operasi), bukan hanya message
log.severe("gagal baca konfigurasi path=$path: ${e.message}")
// Pulihkan dengan strategi eksplisit atau lempar ulang dengan konteks
throw IllegalStateException("konfigurasi tidak dapat dimuat dari $path", e)
}
}
// 4. Lemparkan di layer yang tepat: layer akses data membungkus, layer bisnis mengonsumsi.
class RepoPengguna {
fun cari(id: Int): String {
if (id <= 0) {
// Validation di layer bisnis, bukan ditangkap sebagai kegagalan teknis
throw IllegalArgumentException("id harus positif: $id")
}
if (id > 1000) {
// Kegagalan domain: lempar tipe spesifik
throw PelangganTidakDitemukan(id)
}
return "Pengguna-$id"
}
}
// 5. Untuk expected errors (parse input), pakai Result, bukan exception.
fun parseUmur(teks: String): Result<Int> = runCatching {
teks.trim().toInt().also {
require(it in 0..150) { "umur di luar rentang 0..150: $it" }
}
}
// 6. Global exception handler di main() untuk boundary program.
fun main() {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
// Catat semua kegagalan yang tidak tertangkap sebelum proses berhenti
println("[uncaught] thread=${thread.name} jenis=${throwable::class.simpleName} pesan=${throwable.message}")
}
println("=== Fail fast dengan require ===")
try {
prosesPesanan(-1, "SKU-001", 3)
} catch (e: IllegalArgumentException) {
println("tangkap ${e::class.simpleName}: ${e.message}")
}
println()
println("=== Tipe exception spesifik ===")
try {
prosesPesanan(7, "SKU-001", 10)
} catch (e: StokHabis) {
println("tangkap ${e::class.simpleName}: ${e.message}")
println(" sku=$e")
println(" diminta=${e.diminta}, tersedia=${e.tersedia}")
}
println()
println("=== Log dengan konteks + re-throw wrapping ===")
try {
bacaKonfigurasi("/etc/app.conf")
} catch (e: IllegalStateException) {
println("tangkap ${e::class.simpleName}: ${e.message}")
println(" cause=${e.cause?.javaClass?.simpleName}: ${e.cause?.message}")
}
println()
println("=== Layer yang tepat ===")
val repo = RepoPengguna()
try {
repo.cari(9999)
} catch (e: PelangganTidakDitemukan) {
println("tangkap ${e::class.simpleName}: ${e.message}, id=${e.idPelanggan}")
}
println()
println("=== Result untuk expected errors ===")
for (teks in listOf("25", "abc", "200", " 40 ")) {
val r = parseUmur(teks)
r.fold(
onSuccess = { umur -> println("parse(\"$teks\") -> OK, umur=$umur") },
onFailure = { err -> println("parse(\"$teks\") -> gagal: ${err::class.simpleName}: ${err.message}") },
)
}
}Output yang Dihasilkan:
=== Fail fast dengan require ===
tangkap IllegalArgumentException: idPelanggan harus positif: -1
=== Tipe exception spesifik ===
tangkap StokHabis: stok SKU SKU-001 habis: butuh 10, tersedia 5
sku=StokHabis: stok SKU SKU-001 habis: butuh 10, tersedia 5
diminta=10, tersedia=5
=== Log dengan konteks + re-throw wrapping ===
tangkap IllegalStateException: konfigurasi tidak dapat dimuat dari /etc/app.conf
cause=IOException: file tidak ada: /etc/app.conf
=== Layer yang tepat ===
tangkap PelangganTidakDitemukan: pelanggan id=9999 tidak ditemukan, id=9999
=== Result untuk expected errors ===
parse("25") -> OK, umur=25
parse("abc") -> gagal: NumberFormatException: For input string: "abc"
parse("200") -> gagal: IllegalArgumentException: umur di luar rentang 0..150: 200
parse(" 40 ") -> OK, umur=40Mari kita amati bagian-bagian penting dari kode di atas:
StokHabisdanPelangganTidakDitemukan: Custom exception dengan field konteks (sku,diminta,tersedia,idPelanggan). Pemanggil dapat memproses konteks tanpa memparse pesan string.prosesPesanan: Tigarequiredi awal memvalidasi argumen sebelum logika utama. Bila salah satu false,IllegalArgumentExceptiondilempar dengan pesan yang menyebutkan argumen mana yang salah dan nilainya.bacaKonfigurasi: MenangkapIOException, mencatat log dengan konteks path dan operasi, lalu melempar ulang sebagaiIllegalStateExceptiondengan cause asli. Pola ini tidak menelan exception.RepoPengguna.cari: Memilih tipe exception sesuai sifat kegagalan.IllegalArgumentExceptionuntuk argumen tidak valid,PelangganTidakDitemukanuntuk kegagalan domain. Pemanggil dapat menangkap secara terpisah.parseUmur: MengembalikanResult<Int>lewatrunCatching. Kegagalan parse adalah expected error (input pengguna yang tidak terprediksi), sehinggaResultlebih cocok daripada exception.main: MemasangThread.setDefaultUncaughtExceptionHandleruntuk mencatat kegagalan yang lolos. Handler ini adalah boundary terakhir sebelum proses berhenti, bukan strategi penanganan biasa.
Ringkasan Praktik Terbaik #
Berikut tujuh praktik yang ditunjukkan pada contoh di atas:
| # | Praktik | Manfaat |
|---|---|---|
| 1 | Fail fast dengan require / check | Argumen tidak valid ditolak sebelum logika berjalan; pesan jelas akar masalahnya |
| 2 | Tipe exception spesifik dengan field konteks | Pemanggil dapat memproses kegagalan secara programatik tanpa memparse pesan |
| 3 | Jangan menelan exception diam-diam | Selalu log atau lempar ulang; kegagalan tidak boleh hilang tanpa jejak |
| 4 | Log dengan konteks (operasi, parameter, ID) | Saat membaca log, kita tahu operasi apa dan dengan input apa yang gagal |
| 5 | Lempar di layer yang tepat | Layer bawah membungkus exception teknis ke exception domain; layer atas mengonsumsi |
| 6 | Pakai Result untuk expected errors | Parse input pengguna dan validasi format memakai Result, bukan exception |
| 7 | Global handler di boundary program | Pasang UncaughtExceptionHandler untuk mencatat kegagalan yang lolos sebelum proses berhenti |
Jangan Menelan Exception #
Salah satu kesalahan paling sering adalah menangkap exception lalu mengabaikannya. Pola seperti catch (e: Exception) {} (blok catch kosong) menyembunyikan bug dan menyulitkan diagnosis. Bila Anda yakin ingin mengabaikan, catat setidaknya ke log agar ada jejak saat debugging.
fun bacaAman(path: String): String? {
return try {
readFile(path)
} catch (e: Exception) {
// JANGAN lakukan ini diam-diam
null
}
}
// Lebih baik: catat dengan konteks lalu pulihkan
fun bacaAmanBaik(path: String): String? {
return try {
readFile(path)
} catch (e: IOException) {
log.warning("gagal baca path=$path: ${e.message}")
null
}
}Hindari Exception untuk Control Flow #
Exception mahal karena membangun stack trace, dan dirancang untuk keadaan tak terduga. Memakainya sebagai mekanisme percabangan normal (misal melempar EndOfStreamException di akhir pembacaan) membuat kode sulit dibaca dan performa buruk. Untuk alur yang diprediksi, gunakan Result, null, atau sealed class.
// HINDARI: exception untuk akhir stream
fun bacaItem(): String {
if (tumpukanKosong()) throw EndOfStreamException()
return tumpukan.pop()
}
// LEBIH BAIK: null atau Result
fun bacaItemAman(): String? = if (tumpukanKosong()) null else tumpukan.pop()Referensi Lanjutan #
- Exceptions
— rujukan resmi pola
try-catch-finallyyang idiomatik
Penutup #
Tujuh praktik terbaik di atas membentuk kerangka penanganan kegagalan yang sehat di Kotlin: fail fast di awal, tipe exception spesifik dengan konteks, log yang informatif, layer yang memilih strategi yang tepat, dan Result untuk kegagalan yang merupakan bagian normal dari alur. Dengan menerapkan praktik ini secara konsisten, kode menjadi lebih mudah dipulihkan dan diperiksa. Seluruh section Error Handling telah membawa kita dari hierarki Throwable hingga strategi produksi yang matang.