Saat sebuah function melempar exception dan tidak menangkapnya, exception tersebut akan naik ke pemanggil. Proses ini disebut exception propagation. Pemanggil dapat memilih: menangkap exception tersebut, membiarkannya naik lagi, atau membungkusnya ke dalam exception domain. Pada chapter ini kita akan melihat bagaimana JVM melakukan stack unwinding, membaca stack trace, dan menerapkan pola re-throw dengan cause. Pemahaman ini krusial untuk merancang lapisan aplikasi yang menangani kegagalan secara konsisten.
Stack Unwinding #
Setiap kali sebuah function dipanggil, JVM menambahkan frame baru ke call stack. Frame menyimpan informasi tentang function yang sedang berjalan, parameter lokal, dan lokasi pemanggilan. Saat exception dilempar dan tidak ada catch di function tersebut, JVM mengeluarkan (pop) frame dari call stack dan memeriksa pemanggil. Proses berlanjut naik sampai menemukan catch yang cocok atau sampai frame puncak (main) habis — dalam kasus terakhir, JVM mencetak stack trace dan menghentikan program.
main()
└─ levelAtas()
└─ levelTengah()
└─ levelPalingDalam() <-- exception dilempar di siniSaat levelPalingDalam melempar ArithmeticException, JVM memeriksa levelTengah (tidak ada catch), lalu levelAtas (tidak ada catch), lalu main (ada catch). Setiap langkah ini tercatat di stackTrace exception sebagai array dari objek StackTraceElement.
Propagation Sederhana #
Contoh berikut menunjukkan exception yang merambat naik dari function dalam ke function yang menangkapnya di puncak. Kita juga akan mencetak stack trace untuk memvisualisasikan proses unwinding.
// Demo exception propagation: stack unwinding dari function dalam ke pemanggil luar.
fun levelPalingDalam(): Int {
println("[levelPalingDalam] siap membagi")
val a = 10
val b = 0
// b == 0 menyebabkan ArithmeticException
return a / b
}
fun levelTengah(): Int {
println("[levelTengah] memanggil levelPalingDalam")
// Tidak menangkap exception; exception akan naik
return levelPalingDalam()
}
fun levelAtas(): Int {
println("[levelAtas] memanggil levelTengah")
return levelTengah()
}
fun tangkapDiPuncak(): Int {
return try {
levelAtas()
} catch (e: ArithmeticException) {
println("[tangkapDiPuncak] menangkap ${e::class.simpleName}: ${e.message}")
// Tampilkan stack trace ringkas: 4 baris pertama
val jejak = e.stackTrace.take(4).joinToString("\n ") { elem ->
"${elem.className}.${elem.methodName}(${elem.fileName}:${elem.lineNumber})"
}
println(" Stack trace (4 baris):")
println(" $jejak")
-1
}
}
fun demoReThrowDenganWrapping(): String {
fun bacaDataInternal(): String {
// simulasi kegagalan IO
throw java.io.IOException("koneksi terputus")
}
return try {
bacaDataInternal()
} catch (io: java.io.IOException) {
// Bungkus ke exception domain, pertahankan cause
throw DataLoadException("gagal memuat data pengguna", io)
}
}
class DataLoadException(pesan: String, cause: Throwable) : RuntimeException(pesan, cause)
fun main() {
println("=== Propagation sederhana ===")
val hasil = tangkapDiPuncak()
println("Hasil akhir: $hasil")
println()
println("=== Re-throw dengan wrapping ===")
try {
demoReThrowDenganWrapping()
} catch (e: DataLoadException) {
println("tangkap ${e::class.simpleName}: ${e.message}")
println(" cause = ${e.cause?.javaClass?.simpleName}: ${e.cause?.message}")
}
println()
println("=== Tidak ditangkap sama sekali (akan naik ke JVM) ===")
try {
// Function dalam melempar; pemanggil langsung menangkap
val daftar = listOf(1, 2, 3)
println(daftar[10])
} catch (e: IndexOutOfBoundsException) {
println("tangkap di main: ${e::class.simpleName}")
}
}Output yang Dihasilkan:
=== Propagation sederhana ===
[levelAtas] memanggil levelTengah
[levelTengah] memanggil levelPalingDalam
[levelPalingDalam] siap membagi
[tangkapDiPuncak] menangkap ArithmeticException: / by zero
Stack trace (4 baris):
Exception_propagationKt.levelPalingDalam(exception_propagation.kt:8)
Exception_propagationKt.levelTengah(exception_propagation.kt:14)
Exception_propagationKt.levelAtas(exception_propagation.kt:19)
Exception_propagationKt.tangkapDiPuncak(exception_propagation.kt:24)
Hasil akhir: -1
=== Re-throw dengan wrapping ===
tangkap DataLoadException: gagal memuat data pengguna
cause = IOException: koneksi terputus
=== Tidak ditangkap sama sekali (akan naik ke JVM) ===
tangkap di main: ArrayIndexOutOfBoundsExceptionMari kita bedah bagian-bagian penting dari kode di atas:
levelPalingDalam: MelemparArithmeticExceptionkarena pembagian dengan nol. Function ini tidak memilikicatch, sehingga exception langsung naik.levelTengahdanlevelAtas: Hanya mencetak pesan dan meneruskan pemanggilan. Tanpacatch, exception merambat naik tanpa perubahan.tangkapDiPuncak: MembungkuslevelAtas()dalamtrydan menangkapArithmeticException. Stack trace mencatat urutan pemanggilan:levelPalingDalam→levelTengah→levelAtas→tangkapDiPuncak. Pesan di baris terakhir menunjukkan di mana exception ditangkap.demoReThrowDenganWrapping: MenangkapIOExceptiondari function dalam lalu melemparDataLoadExceptiondengan cause. Pemanggil luar hanya melihat exception domain, namun stack trace asli tetap dapat diakses viae.cause.- Blok
mainketiga: Membuktikan bahwa exception yang tidak ditangkap akan terus naik.listOf(1, 2, 3)[10]melemparIndexOutOfBoundsException, yang langsung ditangkap olehtryluar dimain.
Membaca Stack Trace #
Stack trace adalah jejak pemanggilan saat exception dilempar. Setiap baris menunjukkan class, method, file, dan nomor baris tempat frame berada. Baris pertama adalah titik pelemparan; baris berikutnya adalah pemanggil berurutan ke atas.
Exception_propagationKt.levelPalingDalam(exception_propagation.kt:8)
Exception_propagationKt.levelTengah(exception_propagation.kt:14)
Exception_propagationKt.levelAtas(exception_propagation.kt:19)
Exception_propagationKt.tangkapDiPuncak(exception_propagation.kt:24)Membaca stack trace dari atas ke bawah memberi tahu kita alur pemanggilan yang mengarah ke titik kegagalan. Saat membungkus exception dengan cause, stack trace cause tetap dapat diakses melalui e.cause?.stackTrace, dan stack trace luar menunjukkan titik pembungkusan.
Tangkap atau Lempar #
Setiap function yang melempar exception memiliki tiga pilihan:
| Strategi | Kapan dipakai | Contoh |
|---|---|---|
| Tangkap dan pulihkan | Punya strategi pemulihan konkret | Parse gagal → kembalikan nilai default |
| Tangkap dan bungkus | Exception teknis perlu diterjemahkan ke domain | IOException → DataLoadException |
| Lewati (propagate) | Tidak ada konteks untuk memulihkan; biarkan layer atas menangani | Function utilitas yang tidak tahu tujuan akhir |
Pilihan propagate sering paling tepat di layer utilitas dan layer akses data level rendah. Layer aplikasi, sebaliknya, biasanya memilih tangkap dan bungkus agar exception domain yang bermakna sampai ke pemanggil bisnis.
Membungkus dengan Cause #
Saat membungkas exception, selalu teruskan cause asli sebagai parameter kedua konstruktor Throwable. Tanpa cause, stack trace asli tidak terlihat saat debugging, dan akar masalah menjadi sulit ditelusuri. Pola yang lazim di Kotlin adalah menangkap exception teknis di layer bawah (misal SQLException), lalu melempar exception domain dengan cause yang sama:
class RepoError(pesan: String, cause: Throwable) : RuntimeException(pesan, cause)
fun ambilPengguna(id: Int): Pengguna {
return try {
db.queryPengguna(id) // bisa melempar SQLException
} catch (e: SQLException) {
throw RepoError("gagal mengambil pengguna $id", e)
}
}Pemanggil ambilPengguna hanya melihat RepoError, namun bila terjadi kegagalan, log dapat mencetak e.cause untuk mengetahui akar masalah SQL yang sebenarnya.
Referensi Lanjutan #
- Exceptions — panduan resmi cara exception menjalar naik ke pemanggil
- Throwable API reference
— API reference
causeuntuk membungkus exception asli
Penutup #
Exception propagation memungkinkan kegagalan ditangani di layer yang tepat tanpa memaksa setiap function menangkap. Stack trace memberikan jejak lengkap dari titik kegagalan ke titik penangkapan, dan cause mempertahankan konteks exception asli saat dibungkus. Pemahaman ini mempersiapkan kita untuk membahas perbedaan checked vs unchecked exception dan mengapa Kotlin menghapus pembedaan tersebut — topik yang akan kita dalami pada chapter berikutnya.