Setiap program Kotlin akan menemui kondisi tak terduga: input tidak valid, koneksi jaringan terputus, atau pembagian dengan nol. Bahasa pemrograman menyediakan mekanisme exception untuk menghentikan alur normal dan menyalurkan kegagalan ke blok penanganan. Pada chapter ini kita akan membedah hierarki Throwable, membedakan Error dan Exception, serta melihat bagaimana Kotlin menyederhanakan model exception warisan Java. Pemahaman ini menjadi fondasi sebelum kita menulis blok try-catch pada chapter berikutnya.
Apa Itu Exception #
Exception adalah objek yang merepresentasikan kegagalan saat program berjalan. Saat kondisi tak terduga muncul, runtime JVM membuat objek bertipe Throwable (atau subkelasnya), kemudian melemparkan objek tersebut ke pemanggil melalui mekanisme yang disebut throwing. Jika tidak ada blok penanganan yang cocok di sepanjang tumpukan pemanggilan, JVM akan mencetak stack trace dan menghentikan program.
Konsep penting yang membedakan exception dari nilai balik biasa adalah non-local control flow. Sebuah exception yang dilempar di function dalam akan melompati semua pemanggil di tengah sampai menemukan catch yang cocok. Hal ini membuat kode pemanggil tidak perlu memeriksa status balikan secara manual seperti pola return code pada bahasa C.
Hierarki Throwable #
Seluruh exception di JVM berakar pada kelas Throwable. Dari sini turun dua cabang utama: Error untuk kegagalan fatal level JVM, dan Exception untuk kegagalan aplikasi yang umumnya dapat ditangani. Dari Exception turun lagi cabang RuntimeException yang menjadi induk bagi mayoritas exception tidak terduga seperti NullPointerException dan IllegalArgumentException.
Throwable
├── Error
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
└── Exception
├── IOException
├── SQLException
└── RuntimeException
├── NullPointerException
├── IllegalArgumentException
│ └── NumberFormatException
├── IllegalStateException
└── IndexOutOfBoundsExceptionCabang Error menandai kegagalan struktur JVM itu sendiri, misalnya kehabisan memori atau tumpukan pemanggilan terlalu dalam. Cabang Exception mencakup kegagalan kode aplikasi, mulai dari I/O hingga validasi input. RuntimeException adalah subkelompok exception yang tidak wajib dideklarasikan di signature function di Java.
Error vs Exception #
Error sebaiknya tidak ditangkap di kode aplikasi karena menandakan kondisi yang tidak dapat dipulihkan dari dalam program. Sebagai contoh, OutOfMemoryError berarti heap JVM penuh; menangkap exception tersebut lalu melanjutkan eksekusi tidak membebaskan memori secara otomatis. Pendekatan yang lebih sehat adalah membiarkan JVM menghentikan proses dan menyelidiki akar masalahnya lewat log atau profiler.
Exception, sebaliknya, adalah sinyal yang dirancang untuk ditangani. Kegagalan saat membaca file, parse JSON, atau memvalidasi input layak ditangkap dan dipulihkan dengan strategi seperti nilai cadangan.
Checked vs Unchecked di Java #
Java membagi Exception menjadi dua kategori: checked dan unchecked. Exception yang tidak mewarisi RuntimeException (misalnya IOException dan SQLException) bersifat checked. Java mengharuskan setiap function yang melempar exception checked untuk mendeklarasikannya dengan keyword throws, dan setiap pemanggil wajib menangkap atau mendeklarasikannya kembali. Exception yang mewarisi RuntimeException bersifat unchecked dan bebas dari aturan tersebut.
Aturan checked exception memaksa eksplisititas, namun sering menambah boilerplate. Banyak library Java membungkus exception checked ke RuntimeException agar API lebih ringkas. Kotlin mengikuti filosofi kedua ini secara konsisten.
Kotlin Memperlakukan Semua sebagai Unchecked #
Kotlin tidak memiliki konsep checked exception. Function Kotlin boleh melempar exception apa pun, baik turunan RuntimeException maupun Exception biasa seperti IOException, tanpa mendeklarasikannya di signature. Pemanggil bebas menangkap atau membiarkan exception naik.
// Hierarki Throwable: Error vs Exception, RuntimeException, dan contoh lemparan umum.
fun main() {
// 1. Tampilkan hierarki kelas dari beberapa exception standar.
println("=== Hierarki Exception Standar ===")
val daftarThrowable: List<Throwable> = listOf(
OutOfMemoryError(),
StackOverflowError(),
Exception("kegagalan umum"),
RuntimeException("kegagalan saat runtime"),
NullPointerException("objek null"),
IllegalArgumentException("argumen tidak valid"),
NumberFormatException("format angka salah"),
IllegalStateException("state tidak konsisten"),
IndexOutOfBoundsException("index di luar batas"),
)
for (t in daftarThrowable) {
val jenis = t.javaClass.simpleName
val induk = t.javaClass.superclass.simpleName
val kategori = when (t) {
is Error -> "Error (fatal, jangan ditangkap di kode aplikasi)"
is RuntimeException -> "RuntimeException (unchecked)"
is Exception -> "Exception (bisa checked atau unchecked)"
else -> "Throwable"
}
println("- $jenis extends $induk -> $kategori")
}
println()
println("=== Tangkap NumberFormatException ===")
val teks = "abc"
val angka: Int = try {
teks.toInt()
} catch (e: NumberFormatException) {
println("Gagal parse \"$teks\": ${e.message}")
0
}
println("Hasil akhir angka = $angka")
println()
println("=== Tangkap ArithmeticException ===")
val pembagi = 0
val hasil: Int = try {
100 / pembagi
} catch (e: ArithmeticException) {
println("Tidak boleh bagi nol: ${e.message}")
-1
}
println("Hasil akhir hasil = $hasil")
println()
println("=== Hierarki mulai dari Throwable ===")
val akar = NumberFormatException()
var kelas: Class<*>? = akar.javaClass
while (kelas != null) {
println(" ${kelas.simpleName}")
kelas = kelas.superclass
}
}Output yang Dihasilkan:
=== Hierarki Exception Standar ===
- OutOfMemoryError extends VirtualMachineError -> Error (fatal, jangan ditangkap di kode aplikasi)
- StackOverflowError extends VirtualMachineError -> Error (fatal, jangan ditangkap di kode aplikasi)
- Exception extends Throwable -> Exception (bisa checked atau unchecked)
- RuntimeException extends Exception -> RuntimeException (unchecked)
- NullPointerException extends RuntimeException -> RuntimeException (unchecked)
- IllegalArgumentException extends RuntimeException -> RuntimeException (unchecked)
- NumberFormatException extends IllegalArgumentException -> RuntimeException (unchecked)
- IllegalStateException extends RuntimeException -> RuntimeException (unchecked)
- IndexOutOfBoundsException extends RuntimeException -> RuntimeException (unchecked)
=== Tangkap NumberFormatException ===
Gagal parse "abc": For input string: "abc"
Hasil akhir angka = 0
=== Tangkap ArithmeticException ===
Tidak boleh bagi nol: / by zero
Hasil akhir hasil = -1
=== Hierarki mulai dari Throwable ===
NumberFormatException
IllegalArgumentException
RuntimeException
Exception
Throwable
ObjectMari kita telaah bagian-bagian penting dari kode di atas:
- Daftar
daftarThrowable: Berisi instance dari berbagai exception standar. Konstruksi tanpathrowhanya membuat objek; kita memakainya untuk inspeksi tipe tanpa memicu alur exception. - Operator
is: Cek tipe memanfaatkan polimorfisme.when (t)mencocokkan dari yang paling spesifik (Error,RuntimeException) ke yang umum (Exception) agar label kategori akurat. teks.toInt(): MethodString.toInt()melemparNumberFormatExceptionbila teks bukan angka valid. Kita tangkap dan kembalikan nilai cadangan0.100 / pembagi: Pembagian dengan nol padaIntmemunculkanArithmeticException. Tanpacatch, program akan berhenti dengan exit code non-zero.- Loop
whilemenelusurisuperclass: Membuktikan bahwaNumberFormatExceptionmenurun dariIllegalArgumentException→RuntimeException→Exception→Throwable→Object.
Konstruksi Throwable #
Selain pesan, objek Throwable dapat menyimpan cause (penyebab lain) dan stack trace. Cause dipakai saat kita membungkus exception tingkat rendah ke exception domain aplikasi, sehingga stack trace asli tetap dapat ditelusuri.
import java.io.IOException
fun bacaKonfigurasi(path: String): String {
try {
// simulasi: lempar IOException seperti dari java.io.FileReader
throw IOException("tidak dapat membaca $path")
} catch (io: IOException) {
// bungkus ke exception domain aplikasi, pertahankan cause
throw IllegalStateException("konfigurasi hilang", io)
}
}
fun main() {
try {
bacaKonfigurasi("/etc/app.conf")
} catch (e: IllegalStateException) {
println("Pesan utama : ${e.message}")
println("Cause jenis : ${e.cause?.javaClass?.simpleName}")
println("Cause pesan : ${e.cause?.message}")
}
}Pola di atas lazim dipakai pada layer adapter: exception teknis dari library dibungkus ke exception domain yang lebih bermakna bagi layer atas.
Referensi Lanjutan #
- Exceptions
— panduan resmi hierarki exception dan
Throwable - Throwable API reference
— API reference class
Throwablebeserta cause dan stack trace
Penutup #
Kita sudah memetakan hierarki Throwable, memisahkan Error dari Exception, dan memahami bahwa Kotlin memperlakukan semua exception sebagai unchecked. Pengetahuan ini menjadi pijakan untuk mempelajari blok try-catch.