Exception jarang tertangani di tempat kejadiannya. Sebuah kegagalan parsing mungkin muncul jauh di dalam lapisan penyimpanan, sementara keputusan “tampilkan pesan apa ke pengguna” hanya masuk akal di lapisan antarmuka. Untungnya, exception tidak berhenti di method tempat exception itu lahir: exception merambat naik — propagate — dari method ke pemanggilnya, dari pemanggil ke pemanggil pemanggilnya, sampai bertemu blok catch yang cocok atau sampai ke ujung main. Pada Chapter ini kita ikuti perjalanan itu frame demi frame, mulai dari penangkapan di tengah perjalanan sampai nasib exception yang tidak pernah ditangkap siapa pun.
Exception Naik Frame demi Frame #
Mekanismenya adalah stack unwinding yang sudah kita kenal pada Chapter Konsep Exception, kini dilihat dari sisi perjalanan. Saat exception dilempar, JVM membongkar stack frame method saat ini dan memeriksa pemanggilnya: apakah pemanggilan yang membawa kita ke sini berada di dalam blok try dengan catch yang cocok? Jika tidak, frame pemanggil itu juga dibongkar, dan pemeriksaan naik satu level lagi. Perjalanan ini berhenti di blok catch pertama yang tipenya cocok. Program berikut menyiapkan rantai pemanggilan empat tingkat:
public class PropagasiTertangkap {
static int hitungDiskon(int harga, int persen) {
int diskon = harga * persen / 100;
if (diskon < 0) {
throw new IllegalArgumentException("persentase tidak sah: " + persen);
}
System.out.println("hitungDiskon selesai"); // terlewati saat exception terbang
return diskon;
}
static int prosesItem(int harga, int persen) {
return hitungDiskon(harga, persen);
}
static void jalankanTransaksi() {
int diskon = prosesItem(50000, -10);
System.out.println("diskon: " + diskon);
}
public static void main(String[] args) {
try {
jalankanTransaksi();
} catch (IllegalArgumentException e) {
System.out.println("tertangkap di main: " + e.getMessage());
}
System.out.println("program berlanjut");
}
}Output yang Dihasilkan:
tertangkap di main: persentase tidak sah: -10
program berlanjutTiga method di rantai tengah — hitungDiskon, prosesItem, jalankanTransaksi — tidak memiliki satu blok try pun. Keluaran membuktikan akibatnya: baris hitungDiskon selesai dan diskon: ... tidak pernah tercetak, karena exception melewati frame-frame itu tanpa menjalankan sisa statement-nya. Baris tertangkap di main muncul karena main-lah — satu-satunya yang membungkus pemanggilan dalam try — yang menangkap, dan setelah itu program berlanjut normal. Peta perjalanannya dapat digambar begini:
Ketika Tidak Ada Penangkap #
Apa nasib exception kalau tidak ada satu pun penangkap di sepanjang rantai? Program berikut memakai rantai pemanggilan yang sama, tetapi main tidak lagi membungkus pemanggilan dalam try:
public class PropagasiLepas {
static int hitungDiskon(int harga, int persen) {
int diskon = harga * persen / 100;
if (diskon < 0) {
throw new IllegalArgumentException("persentase tidak sah: " + persen);
}
return diskon;
}
static int prosesItem(int harga, int persen) {
return hitungDiskon(harga, persen);
}
static void jalankanTransaksi() {
int diskon = prosesItem(50000, -10);
System.out.println("diskon: " + diskon);
}
public static void main(String[] args) {
jalankanTransaksi();
System.out.println("program berlanjut");
}
}Program berhenti dengan exit code 1 sebelum mencetak apa pun. Default handler JVM mengambil alih di ujung main dan melaporkan kegagalan lewat stderr: baris pembukanya berbunyi Exception in thread "main" java.lang.IllegalArgumentException: persentase tidak sah: -10, dan stack trace di bawahnya mencatat empat frame berurutan — hitungDiskon, lalu prosesItem, lalu jalankanTransaksi, lalu main — masing-masing dengan nama berkas dan nomor baris pemanggilannya. Stack trace itulah potret perjalanan propagasi yang barusan kita bahas: frame paling atas adalah titik pelemparan, frame di bawahnya adalah jalur yang dilalui exception saat naik. Membaca stack trace dari atas ke bawah sama dengan membaca perjalanan exception dari tempat lahir hingga ke ujung program.
Propagasi Checked Exception Lewat throws #
Untuk checked exception, propagasi meninggalkan jejak yang terlihat di tanda tangan: setiap method yang melewatkan kemungkinan kegagalan tanpa menangkapnya wajib menyatakan klausa throws. Pola rantainya tampak seperti ini:
import java.io.IOException;
public class RantaiChecked {
static String bacaData() throws IOException {
// ... pembacaan yang bisa gagal ...
return "data";
}
static String muatLaporan() throws IOException {
return bacaData(); // tidak menangkap: wajib menyatakan ulang
}
static void cetakLaporan() throws IOException {
System.out.println(muatLaporan()); // masih diteruskan ke atas
}
}Ketiga method membentuk jalur propagasi IOException dari sumber kegagalan hingga ke pemanggil terluar; compiler memastikan tidak ada satu pun simpul yang diam-diam melupakan kemungkinan itu. Kalau cetakLaporan kelak memutuskan menangkap sendiri dengan try-catch, klausa throws-nya boleh dihapus — propagasi berhenti di method tersebut, dan pemanggil di atasnya tidak lagi terbebani. Inilah cara Java menjaga kontrak kegagalan tetap eksplisit di sepanjang rantai pemanggilan, tanpa bergantung pada dokumentasi yang mudah kedaluwarsa.
Perjalanan exception, ternyata, tidak selalu menyisakan satu sinyal tunggal. Pada try-with-resources, sebuah perjalanan bisa membawa satu exception utama sekaligus beberapa exception lain yang tertindih di belakangnya. Chapter berikutnya membahas suppressed exceptions — exception yang ikut terjadi namun tidak diperbolehkan menimpa sinyal utama.
Referensi #
- Class Throwable pada API java.lang
- Oracle: Exceptions di dev.java
- Java Language Specification bab 11: Exceptions
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PropagasiLepas.java
Unduh berkaspublic class PropagasiLepas {
static int hitungDiskon(int harga, int persen) {
int diskon = harga * persen / 100;
if (diskon < 0) {
throw new IllegalArgumentException("persentase tidak sah: " + persen);
}
return diskon;
}
static int prosesItem(int harga, int persen) {
return hitungDiskon(harga, persen);
}
static void jalankanTransaksi() {
int diskon = prosesItem(50000, -10);
System.out.println("diskon: " + diskon);
}
public static void main(String[] args) {
jalankanTransaksi();
System.out.println("program berlanjut");
}
}
PropagasiTertangkap.java
Unduh berkaspublic class PropagasiTertangkap {
static int hitungDiskon(int harga, int persen) {
int diskon = harga * persen / 100;
if (diskon < 0) {
throw new IllegalArgumentException("persentase tidak sah: " + persen);
}
System.out.println("hitungDiskon selesai"); // terlewati saat exception terbang
return diskon;
}
static int prosesItem(int harga, int persen) {
return hitungDiskon(harga, persen);
}
static void jalankanTransaksi() {
int diskon = prosesItem(50000, -10);
System.out.println("diskon: " + diskon);
}
public static void main(String[] args) {
try {
jalankanTransaksi();
} catch (IllegalArgumentException e) {
System.out.println("tertangkap di main: " + e.getMessage());
}
System.out.println("program berlanjut");
}
}