Eksepsi #
Exception adalah cara program memberi sinyal bahwa sesuatu yang tidak diharapkan telah terjadi — file tidak ditemukan, koneksi terputus, input tidak valid, atau operasi yang tidak bisa diselesaikan. Menangani exception dengan benar adalah perbedaan antara aplikasi yang crash secara misterius dan aplikasi yang gagal dengan cara yang terkendali dan informatif. Kotlin mewarisi mekanisme exception dari JVM, tapi dengan beberapa perbedaan penting dari Java: semua exception adalah unchecked, try adalah ekspresi yang bisa mengembalikan nilai, dan tersedia fungsi seperti runCatching untuk gaya yang lebih fungsional. Artikel ini membahas semua aspek penanganan exception di Kotlin secara mendalam.
Anatomi Exception di Kotlin #
Setiap exception adalah objek yang mewarisi dari kelas Throwable. Hierarki di JVM terbagi dua: Error (masalah kritis JVM, tidak bisa ditangani) dan Exception (kondisi yang bisa dan harus ditangani).
flowchart TD
A[Throwable] --> B[Error]
A --> C[Exception]
B --> D[OutOfMemoryError]
B --> E[StackOverflowError]
C --> F[RuntimeException]
C --> G[IOException]
F --> H[NullPointerException]
F --> I[IllegalArgumentException]
F --> J[IllegalStateException]
F --> K[IndexOutOfBoundsException]
F --> L[ArithmeticException]Exception paling umum yang akan kamu temui di Kotlin sehari-hari adalah subclass dari RuntimeException — semuanya unchecked dan tidak perlu dideklarasikan di signature fungsi.
Blok try-catch
#
try-catch menangkap exception yang dilempar dalam blok try dan mengeksekusi blok catch yang sesuai:
fun bagi(a: Int, b: Int): Int {
try {
return a / b
} catch (e: ArithmeticException) {
println("Error: ${e.message}")
return 0
}
}
println(bagi(10, 2)) // 5
println(bagi(10, 0)) // Error: / by zero, lalu 0
Multiple Catch Block #
Kamu bisa menangkap berbagai jenis exception dengan beberapa blok catch. Urutkan dari yang paling spesifik ke yang paling umum:
fun bacaFile(path: String): String {
try {
val file = java.io.File(path)
return file.readText()
} catch (e: java.io.FileNotFoundException) {
println("File tidak ditemukan: $path")
return ""
} catch (e: java.io.IOException) {
println("Gagal membaca file: ${e.message}")
return ""
} catch (e: SecurityException) {
println("Tidak punya izin membaca: $path")
return ""
} catch (e: Exception) {
// Tangkap semua exception lain — hati-hati dengan pola ini
println("Error tidak terduga: ${e.javaClass.simpleName}: ${e.message}")
return ""
}
}
Menangkap Multiple Exception dalam Satu Catch #
Kotlin mengizinkan menangkap beberapa tipe exception sekaligus dengan |:
fun prosesTeks(input: String): Int {
return try {
input.trim().toInt()
} catch (e: NumberFormatException) {
-1
} catch (e: NullPointerException) {
-1
}
}
// Cara lebih ringkas dengan multi-catch (sejak Java 7, didukung di Kotlin via JVM)
// Kotlin tidak punya sintaks | untuk ini, tapi bisa gunakan Exception bersama is check:
fun prosesTeksDua(input: String?): Int {
return try {
input!!.trim().toInt()
} catch (e: Exception) {
when (e) {
is NumberFormatException -> { println("Bukan angka: $input"); -1 }
is NullPointerException -> { println("Input null"); -2 }
else -> throw e // lempar ulang yang tidak dikenal
}
}
}
try sebagai Ekspresi
#
Di Kotlin, try adalah ekspresi yang mengembalikan nilai — nilai dari baris terakhir blok try atau catch yang dieksekusi. Ini memungkinkan kode yang lebih ringkas.
// try sebagai ekspresi — tanpa return eksplisit
fun parseAngka(teks: String): Int {
return try {
teks.toInt()
} catch (e: NumberFormatException) {
0 // nilai default jika gagal
}
}
// Langsung assign ke variabel
val angka = try {
"42abc".toInt()
} catch (e: NumberFormatException) {
-1
}
println(angka) // -1
// Digunakan langsung dalam ekspresi
val validUmur = try { inputUmur.toInt() } catch (e: Exception) { 0 }
println(if (validUmur >= 18) "Dewasa" else "Anak-anak")
Blok finally
#
Blok finally selalu dieksekusi — baik exception terjadi maupun tidak, baik ada return di tengah try maupun tidak. Ini digunakan untuk memastikan resource selalu dibersihkan.
fun aksesDatabase(query: String): String {
val koneksi = bukaKoneksi()
try {
return koneksi.eksekusi(query)
} catch (e: Exception) {
println("Query gagal: ${e.message}")
return ""
} finally {
koneksi.tutup() // SELALU dieksekusi
println("Koneksi ditutup")
}
}
use — Alternatif finally untuk Resource
#
Untuk objek yang mengimplementasikan Closeable atau AutoCloseable (seperti file, koneksi, stream), Kotlin menyediakan fungsi use yang otomatis menutup resource meski terjadi exception. Ini jauh lebih bersih dari try-finally manual:
// ANTI-PATTERN: try-finally manual untuk menutup resource
fun bacaFileLama(path: String): String {
val reader = java.io.BufferedReader(java.io.FileReader(path))
try {
return reader.readLine() ?: ""
} finally {
reader.close()
}
}
// BENAR: gunakan use — lebih bersih dan aman
fun bacaFile(path: String): String {
return java.io.File(path).bufferedReader().use { reader ->
reader.readText()
}
}
// Beberapa resource sekaligus
fun salinFile(dari: String, ke: String) {
java.io.FileInputStream(dari).use { input ->
java.io.FileOutputStream(ke).use { output ->
input.copyTo(output)
}
}
}
Melempar Exception dengan throw
#
Gunakan throw untuk melempar exception secara eksplisit ketika kondisi yang tidak valid terdeteksi:
fun validasiUmur(umur: Int) {
if (umur < 0) throw IllegalArgumentException("Umur tidak boleh negatif: $umur")
if (umur > 150) throw IllegalArgumentException("Umur tidak masuk akal: $umur")
}
fun ambilElemen(daftar: List<String>, indeks: Int): String {
if (daftar.isEmpty()) throw NoSuchElementException("Daftar kosong")
if (indeks < 0 || indeks >= daftar.size) {
throw IndexOutOfBoundsException("Indeks $indeks di luar rentang 0..${daftar.size - 1}")
}
return daftar[indeks]
}
throw sebagai Ekspresi
#
Di Kotlin, throw adalah ekspresi bertipe Nothing — ia bisa digunakan di sisi kanan Elvis operator atau ekspresi kondisional:
// throw dalam Elvis operator — validasi ringkas
fun ambilKonfigurasi(kunci: String): String {
return System.getenv(kunci)
?: throw IllegalStateException("Environment variable '$kunci' tidak ditemukan")
}
// throw dalam if ekspresi
fun proses(input: String?): String {
val teks = input ?: throw NullPointerException("Input tidak boleh null")
return teks.uppercase()
}
// throw dalam when
fun tanganiStatus(kode: Int): String = when (kode) {
200 -> "OK"
404 -> "Tidak ditemukan"
500 -> "Error server"
else -> throw IllegalArgumentException("Kode HTTP tidak dikenal: $kode")
}
require, check, dan error — Validasi Idiomatis
#
Kotlin menyediakan fungsi bawaan untuk validasi yang lebih ekspresif dari throw manual:
// require() — validasi parameter fungsi
// Melempar IllegalArgumentException jika kondisi false
fun buatSegi(sisi: Double): Double {
require(sisi > 0) { "Sisi harus positif, dapat: $sisi" }
return sisi * sisi
}
// requireNotNull() — validasi tidak null
fun prosesPengguna(pengguna: Pengguna?) {
val p = requireNotNull(pengguna) { "Pengguna tidak boleh null" }
println("Memproses: ${p.nama}")
}
// check() — validasi state objek
// Melempar IllegalStateException jika kondisi false
class Printer {
private var isOn = false
fun nyalakan() { isOn = true }
fun cetak(dokumen: String) {
check(isOn) { "Printer harus dinyalakan sebelum mencetak" }
println("Mencetak: $dokumen")
}
}
// error() — lempar IllegalStateException dengan pesan
fun ambilAdmin(daftar: List<Pengguna>): Pengguna {
return daftar.firstOrNull { it.peran == "ADMIN" }
?: error("Tidak ada admin ditemukan dalam sistem")
}
Perbedaan require vs check:
| Fungsi | Exception | Digunakan untuk |
|---|---|---|
require(kondisi) | IllegalArgumentException | Validasi argumen/parameter fungsi |
requireNotNull(nilai) | IllegalArgumentException | Validasi argumen tidak null |
check(kondisi) | IllegalStateException | Validasi state objek sebelum operasi |
checkNotNull(nilai) | IllegalStateException | Validasi state tidak null |
error(pesan) | IllegalStateException | Kondisi yang seharusnya tidak pernah terjadi |
Custom Exception #
Buat custom exception dengan mewarisi dari Exception atau subclass-nya. Ini memungkinkan error handling yang lebih spesifik dan deskriptif:
// Exception dasar untuk domain aplikasi
open class AplikasiException(
pesan: String,
penyebab: Throwable? = null
) : Exception(pesan, penyebab)
// Hierarki exception yang lebih spesifik
class PenggunaNotFoundException(val id: Long) :
AplikasiException("Pengguna dengan ID $id tidak ditemukan")
class EmailSudahTerdaftarException(val email: String) :
AplikasiException("Email '$email' sudah digunakan akun lain")
class SaldoTidakCukupException(
val saldoAda: Double,
val dibutuhkan: Double
) : AplikasiException(
"Saldo tidak cukup: punya Rp${"%,.0f".format(saldoAda)}, " +
"butuh Rp${"%,.0f".format(dibutuhkan)}"
)
class BatasPercobaaanException(val maxPercobaan: Int) :
AplikasiException("Akun dikunci setelah $maxPercobaan percobaan login gagal")
Menggunakan Custom Exception Hierarchy #
class LayananPengguna(private val repo: PenggunaRepository) {
fun login(email: String, sandi: String): Token {
val pengguna = repo.cariByEmail(email)
?: throw PenggunaNotFoundException(0) // ID tidak diketahui saat email tidak ada
if (pengguna.percobaanLogin >= 5) {
throw BatasPercobaaanException(5)
}
if (!pengguna.verifikasiSandi(sandi)) {
repo.tambahPercobaanLogin(pengguna.id)
throw AplikasiException("Email atau sandi salah")
}
return buatToken(pengguna)
}
}
// Penanganan yang terstruktur di layer atas
fun tanganiLogin(email: String, sandi: String) {
try {
val token = layanan.login(email, sandi)
println("Login berhasil, token: ${token.nilai}")
} catch (e: BatasPercobaaanException) {
println("Akun terkunci: ${e.message}")
println("Hubungi dukungan untuk membuka kunci")
} catch (e: PenggunaNotFoundException) {
println("Akun tidak ditemukan") // pesan generik untuk keamanan
} catch (e: AplikasiException) {
println("Login gagal: ${e.message}")
} catch (e: Exception) {
println("Terjadi kesalahan sistem, coba lagi nanti")
// log e.stackTrace ke sistem monitoring
}
}
runCatching — Pendekatan Fungsional
#
runCatching adalah alternatif fungsional untuk try-catch. Ia mengembalikan objek Result<T> yang merepresentasikan keberhasilan atau kegagalan:
// Dasar penggunaan
val hasil = runCatching { "42".toInt() }
println(hasil.isSuccess) // true
println(hasil.getOrNull()) // 42
val gagal = runCatching { "bukan angka".toInt() }
println(gagal.isFailure) // true
println(gagal.getOrNull()) // null
println(gagal.exceptionOrNull()?.message) // For input string: "bukan angka"
// Dengan nilai default
val angka = runCatching { "abc".toInt() }.getOrDefault(0)
println(angka) // 0
// Dengan transformasi error
val pesan = runCatching { "abc".toInt() }
.getOrElse { e -> -1 }
println(pesan) // -1
// Chaining operasi
val hasilAkhir = runCatching { bacaFile("/data/config.json") }
.map { isiFile -> parseJson(isiFile) }
.recover { e ->
println("Gagal baca file: ${e.message}, pakai konfigurasi default")
KonfigurasiDefault()
}
.getOrThrow()
runCatching vs try-catch
#
GUNAKAN try-catch jika:
✓ Butuh tangani berbagai tipe exception secara berbeda
✓ Perlu blok finally untuk cleanup
✓ Kode imperatif yang sudah familiar di tim
✓ Perlu re-throw exception setelah logging
GUNAKAN runCatching jika:
✓ Operasi yang mungkin gagal dan hasilnya diproses lebih lanjut
✓ Ingin transformasi hasil dengan map/recover secara deklaratif
✓ Kode fungsional yang berantai (pipeline)
✓ Ingin nilai default yang elegan tanpa try-catch verbose
Exception di Kotlin vs Java #
Perbedaan terpenting antara Kotlin dan Java dalam hal exception:
| Aspek | Java | Kotlin |
|---|---|---|
| Checked exception | Ada — wajib ditangkap atau dideklarasikan dengan throws | Tidak ada — semua unchecked |
try sebagai ekspresi | Tidak | Ya — mengembalikan nilai |
throw sebagai ekspresi | Tidak | Ya — bertipe Nothing |
Deklarasi throws | Wajib untuk checked exception | Opsional @Throws untuk interop Java |
| Resource cleanup | try-finally atau try-with-resources | use {} (lebih bersih) |
Karena Kotlin tidak punya checked exception, kamu tidak pernah dipaksa untuk menulis try-catch hanya agar kode bisa dikompilasi. Ini mengurangi boilerplate tapi juga berarti kamu harus lebih disiplin dalam mendokumentasikan exception yang mungkin dilempar oleh fungsimu.
Jika fungsi Kotlin perlu dipanggil dari kode Java, gunakan anotasi
@Throwsagar Java tahu exception apa yang bisa dilempar:@Throws(IOException::class, IllegalArgumentException::class) fun bacaKonfigurasi(path: String): String { // implementasi }
Kapan Tidak Menggunakan Exception #
Exception memiliki biaya — baik dari sisi performa (membuat stack trace mahal) maupun keterbacaan kode. Jangan gunakan exception untuk alur kontrol normal.
// ANTI-PATTERN: exception untuk alur kontrol normal
fun cariPengguna(id: Long): Pengguna {
return repo.cariById(id) ?: throw PenggunaNotFoundException(id)
}
// Pemanggil harus try-catch hanya untuk cek "ada atau tidak"
try {
val pengguna = cariPengguna(42L)
tampilkan(pengguna)
} catch (e: PenggunaNotFoundException) {
tampilkanPesan("Pengguna tidak ditemukan")
}
// BENAR: kembalikan null untuk "tidak ditemukan", exception untuk error sungguhan
fun cariPengguna(id: Long): Pengguna? = repo.cariById(id)
// Pemanggil cukup gunakan Elvis atau let
val pengguna = cariPengguna(42L)
if (pengguna != null) {
tampilkan(pengguna)
} else {
tampilkanPesan("Pengguna tidak ditemukan")
}
// Atau dengan runCatching untuk operasi yang bisa gagal secara legitimate
val koneksiDb = runCatching { bukaKoneksi() }
.getOrElse { e ->
log.error("Gagal konek DB", e)
return@fungsi // atau kembalikan nilai default
}
Panduan kapan menggunakan exception vs nilai kembalian nullable:
GUNAKAN exception jika:
✓ Kondisi benar-benar tidak diharapkan dan tidak bisa dipulihkan normal
✓ Error yang perlu ditangani di layer yang jauh di atas (cross-cutting concern)
✓ Konstruksi objek gagal karena parameter tidak valid
✓ Violation kontrak yang seharusnya tidak pernah terjadi
KEMBALIKAN null atau sealed class jika:
✓ "Tidak ditemukan" adalah hasil yang valid dan diprediksi
✓ Operasi opsional yang mungkin berhasil atau tidak
✓ Validasi input dari pengguna (lebih baik kembalikan pesan error)
✓ Alur kontrol yang diprediksi sebelumnya
Ringkasan #
tryadalah ekspresi — di Kotlin,trymengembalikan nilai dari blok yang dieksekusi. Gunakan ini untuk mengurangi variabel sementara dan kode yang lebih deklaratif.- Semua exception adalah unchecked — tidak ada checked exception di Kotlin. Kamu tidak pernah dipaksa compiler untuk menangkap exception, tapi itu berarti kamu harus lebih proaktif mendokumentasikan fungsi yang bisa melempar.
use {}untuk resource cleanup — gantikantry-finallymanual denganuse {}untuk semua objekCloseable. Lebih bersih dan tidak bisa lupa menutup resource.requiredancheckuntuk validasi idiomatis — gunakanrequire()untuk validasi parameter (lemparIllegalArgumentException) dancheck()untuk validasi state objek (lemparIllegalStateException).- Custom exception hierarchy — buat hirarki exception yang mencerminkan domain aplikasimu. Ini memungkinkan penanganan yang spesifik dan pesan error yang informatif.
- Tangkap yang paling spesifik terlebih dahulu — urutan blok
catchdari tipe yang paling spesifik ke paling umum.Exceptionsebagai catch-all harus selalu paling terakhir.runCatchinguntuk gaya fungsional — gunakanrunCatchingketika ingin memproses hasil operasi yang mungkin gagal secara deklaratif denganmap,recover, dangetOrElse.- Jangan pakai exception untuk alur kontrol — kembalikan
nullatau sealed class untuk kondisi yang diprediksi (“tidak ditemukan”). Simpan exception untuk kondisi yang benar-benar tidak terduga dan tidak bisa dipulihkan secara normal.