Istilah error dan exception sering dipakai bergantian dalam percakapan sehari-hari, namun di JVM keduanya memiliki arti teknis yang berbeda. Error menandakan kegagalan fatal level JVM yang umumnya tidak boleh ditangkap di kode aplikasi, sedangkan Exception menandakan kegagalan yang dapat dipulihkan. Pada chapter ini kita akan memetakan kedua kategori tersebut, memisahkan programming error dari business error, dan menentukan strategi penanganan yang sesuai. Pemahaman ini mencegah kode menangkap hal yang seharusnya dibiarkan, dan sebaliknya.
Hierarki Throwable Sekali Lagi #
Di puncak hierarki adalah Throwable. Dari sini turun dua cabang utama: Error dan Exception. Cabang Error mencakup kegagalan yang terjadi di luar kendali kode aplikasi, sementara cabang Exception mencakup kegagalan yang dirancang untuk ditangani. Pada chapter Exception Fundamentals kita sudah melihat skema ini; sekarang kita akan fokus pada implikasi praktisnya.
Throwable
├── Error (fatal, JVM-level)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
└── Exception (dapat dipulihkan)
├── RuntimeException (programming & business errors)
└── Exception lain (IOException, SQLException, ...)Error: Kondisi Fatal #
Error dilempar oleh JVM ketika sistem tidak dapat melanjutkan eksekusi secara wajar. Contoh paling dikenal adalah OutOfMemoryError (heap penuh) dan StackOverflowError (rekursi terlalu dalam). Karena akar masalah berada di luar kode aplikasi, menangkap Error tidak bermanfaat — kecuali untuk mencatat log lalu melempar ulang. Bahkan menangkap pun sering kali berbahaya karena aplikasi mungkin dalam kondisi tidak konsisten.
fun prosesBesar(): Nothing {
// simulasi kondisi kehabisan memori
throw OutOfMemoryError("heap habis (simulasi)")
}
// PENDEKATAN YANG DIREKOMENDASIKAN: biarkan naik ke JVM,
// perbaiki konfigurasi JVM atau tutup kebocoran resource.Exception: Kegagalan yang Dapat Dipulihkan #
Exception dirancang untuk ditangani. Kegagalan ini biasanya berasal dari input pengguna, kondisi lingkungan, atau state aplikasi yang tidak valid. Cabang RuntimeException mencakup unchecked exception seperti NullPointerException, IllegalArgumentException, dan ArithmeticException. Cabang Exception lain mencakup checked exception Java seperti IOException dan SQLException.
// Demo Error vs Exception: kategori Throwable, kapan menangkap, kapan membiarkan.
import java.io.IOException
class DomainException(pesan: String) : RuntimeException(pesan)
fun bagi(a: Int, b: Int): Int {
if (b == 0) {
// Exception: kegagalan aplikasi yang dapat ditangkap
throw ArithmeticException("pembagi nol")
}
return a / b
}
fun aksesElemen(index: Int): String {
val daftar = listOf("A", "B", "C")
if (index < 0 || index >= daftar.size) {
// Programming error: IndexOutOfBoundsException
throw IndexOutOfBoundsException("index $index di luar 0..${daftar.size - 1}")
}
return daftar[index]
}
fun validasiBisnis(umur: Int): String {
// Business error: validasi input pengguna, ditangkap di layer UI/controller
if (umur < 0 || umur > 150) {
throw DomainException("umur di luar rentang 0..150")
}
return when {
umur < 18 -> "anak"
else -> "dewasa"
}
}
fun simulasiKebocoranMemori(): Nothing {
// Error: kondisi fatal JVM, JANGAN ditangkap di kode aplikasi
throw OutOfMemoryError("heap habis (simulasi)")
}
fun main() {
println("=== Exception: bisa ditangkap ===")
try {
bagi(10, 0)
} catch (e: ArithmeticException) {
println("tangkap ${e::class.simpleName}: ${e.message}")
}
println()
println("=== Programming error (IndexOutOfBoundsException) ===")
try {
aksesElemen(10)
} catch (e: IndexOutOfBoundsException) {
println("tangkap ${e::class.simpleName}: ${e.message}")
}
println()
println("=== Business error (DomainException) ===")
try {
validasiBisnis(200)
} catch (e: DomainException) {
println("tangkap ${e::class.simpleName}: ${e.message}")
}
println()
println("=== Kategori Error: jangan ditangkap di app ===")
println("Throwable")
println(" ├── Error (fatal, JVM-level: OOM, StackOverflow)")
println(" │ Jangan ditangkap; biarkan proses berhenti, perbaiki akar masalah.")
println(" └── Exception")
println(" ├── RuntimeException (unchecked; programming/business errors)")
println(" └── Exception lain (I/O, SQL, dsb.)")
println()
println("=== Penanganan terbaik ===")
val kategori = mapOf(
"OutOfMemoryError" to "Error: perbaiki konfigurasi JVM atau tutup kebocoran resource",
"StackOverflowError" to "Error: perbaiki rekursi tak hingga",
"NullPointerException" to "Programming error: perbaiki asumsi null di kode",
"IllegalArgumentException" to "Programming error: validasi argumen dari pemanggil",
"IOException" to "Exception: tangkap, retry, atau fallback",
"DomainException" to "Business error: konversi ke pesan pengguna",
)
for ((nama, saran) in kategori) {
println("- $nama -> $saran")
}
}Output yang Dihasilkan:
=== Exception: bisa ditangkap ===
tangkap ArithmeticException: pembagi nol
=== Programming error (IndexOutOfBoundsException) ===
tangkap IndexOutOfBoundsException: index 10 di luar 0..2
=== Business error (DomainException) ===
tangkap DomainException: umur di luar rentang 0..150
=== Kategori Error: jangan ditangkap di app ===
Throwable
├── Error (fatal, JVM-level: OOM, StackOverflow)
│ Jangan ditangkap; biarkan proses berhenti, perbaiki akar masalah.
└── Exception
├── RuntimeException (unchecked; programming/business errors)
└── Exception lain (I/O, SQL, dsb.)
=== Penanganan terbaik ===
- OutOfMemoryError -> Error: perbaiki konfigurasi JVM atau tutup kebocoran resource
- StackOverflowError -> Error: perbaiki rekursi tak hingga
- NullPointerException -> Programming error: perbaiki asumsi null di kode
- IllegalArgumentException -> Programming error: validasi argumen dari pemanggil
- IOException -> Exception: tangkap, retry, atau fallback
- DomainException -> Business error: konversi ke pesan penggunaMari kita amati bagian-bagian penting dari kode di atas:
DomainException: Custom exception yang mewarisiRuntimeException. Dipakai untuk merepresentasikan kegagalan aturan bisnis, misal validasi rentang umur pengguna.bagi: MelemparArithmeticExceptionbila pembagi nol. Termasuk programming error — pemanggil seharusnya memeriksa kondisi sebelum memanggil, atau menangkap bila memang tidak bisa dihindari.aksesElemen: MelemparIndexOutOfBoundsException. Ini programming error: pemanggil mengirim index di luar batas. Cara terbaik adalah mencegah di sisi pemanggil, bukan menangkap di sisi function.validasiBisnis: MelemparDomainException. Termasuk business error — input pengguna yang tidak memenuhi aturan bisnis. Layer UI atau controller harus menangkap dan mengonversi ke pesan pengguna.simulasiKebocoranMemori: MelemparOutOfMemoryError. Termasuk kategoriError— fatal, tidak boleh ditangkap di kode aplikasi. Function hanya ada untuk demonstrasi.
Programming Error vs Business Error #
Dalam kategori Exception, terutama RuntimeException, kita bisa lagi memisahkan dua jenis kegagalan:
| Jenis | Penyebab | Contoh | Penanganan ideal |
|---|---|---|---|
| Programming error | Bug di kode: asumsi salah, kontrak function dilanggar | NullPointerException, IndexOutOfBoundsException, IllegalArgumentException | Perbaiki kode pemanggil. Jangan tangkap sebagai strategi biasa. |
| Business error | Input pengguna tidak memenuhi aturan bisnis | DomainException, custom validation exception | Tangkap di layer UI/controller, konversi ke pesan yang ditampilkan pengguna. |
Pemisahan ini tidak tercermin di tipe exception, namun memengaruhi strategi penanganan. Programming error sebaiknya dibiarkan naik sampai batas tertentu (misalnya global exception handler), karena menangkapnya hanya menyembunyikan bug. Business error sebaiknya ditangkap sedini mungkin di layer yang dekat dengan pengguna.
Strategi Penanganan #
Berdasarkan kategori kegagalan, berikut strategi yang direkomendasikan:
fun tangani(io: IOException) {
// Exception dari lingkungan: retry, fallback, atau laporkan
}
fun tanganiProgramming(state: IllegalStateException) {
// Bug: catat stack trace lengkap, jangan dipulihkan diam-diam
}
fun tanganiBisnis(domain: DomainException) {
// Konversi pesan ke format pengguna, tampilkan di UI
}Untuk Error, pendekatan terbaik adalah tidak menangkap sama sekali. Bila aplikasi memerlukan global handler untuk mencatat kegagalan sebelum proses berhenti (misalnya di server), pasang di level uncaught exception handler JVM, bukan di setiap titik kode bisnis.
Referensi Lanjutan #
- Exceptions
— panduan resmi pemisahan
ErrordariException - Throwable API reference
— API reference
Throwablesebagai superclass kedua kategori
Penutup #
Pemisahan Error dari Exception, dan programming error dari business error, memandu kita memilih strategi penanganan yang tepat. Error sebaiknya dibiarkan naik ke JVM; programming error sebaiknya diperbaiki di sisi pemanggil; business error ditangkap dan dikonversi ke pesan pengguna. Dengan pemahaman ini, kita siap merangkum seluruh chapter menjadi praktik terbaik penanganan kegagalan di Kotlin pada chapter penutup.