Eksepsi

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:

FungsiExceptionDigunakan untuk
require(kondisi)IllegalArgumentExceptionValidasi argumen/parameter fungsi
requireNotNull(nilai)IllegalArgumentExceptionValidasi argumen tidak null
check(kondisi)IllegalStateExceptionValidasi state objek sebelum operasi
checkNotNull(nilai)IllegalStateExceptionValidasi state tidak null
error(pesan)IllegalStateExceptionKondisi 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:

AspekJavaKotlin
Checked exceptionAda — wajib ditangkap atau dideklarasikan dengan throwsTidak ada — semua unchecked
try sebagai ekspresiTidakYa — mengembalikan nilai
throw sebagai ekspresiTidakYa — bertipe Nothing
Deklarasi throwsWajib untuk checked exceptionOpsional @Throws untuk interop Java
Resource cleanuptry-finally atau try-with-resourcesuse {} (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 @Throws agar 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 #

  • try adalah ekspresi — di Kotlin, try mengembalikan 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 — gantikan try-finally manual dengan use {} untuk semua objek Closeable. Lebih bersih dan tidak bisa lupa menutup resource.
  • require dan check untuk validasi idiomatis — gunakan require() untuk validasi parameter (lempar IllegalArgumentException) dan check() untuk validasi state objek (lempar IllegalStateException).
  • 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 catch dari tipe yang paling spesifik ke paling umum. Exception sebagai catch-all harus selalu paling terakhir.
  • runCatching untuk gaya fungsional — gunakan runCatching ketika ingin memproses hasil operasi yang mungkin gagal secara deklaratif dengan map, recover, dan getOrElse.
  • Jangan pakai exception untuk alur kontrol — kembalikan null atau sealed class untuk kondisi yang diprediksi (“tidak ditemukan”). Simpan exception untuk kondisi yang benar-benar tidak terduga dan tidak bisa dipulihkan secara normal.

← Sebelumnya: Interface   Berikutnya: List →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact