Ketika lapisan bawah melempar NumberFormatException tetapi lapisan atas hanya mengerti bahasa domain — “pengaturan rusak” — ada godaan untuk membuat exception baru dan melupakan yang lama. Sayangnya, itulah cara paling umum kehilangan akar masalah: pemelihara aplikasi melihat PengaturanRusakException tanpa petunjuk apa yang sebenarnya salah, dan penelusuran berhenti di tempat pembungkusan, bukan di tempat kejadian. Exception chaining menutup celah itu: exception baru menyimpan exception lama sebagai cause, dan getCause() mengembalikannya kapan pun kita butuh. Pada Chapter ini kita pelajari cara membungkus dengan cause, menelusuri rantainya, dan kapan pembungkusan memang tepat.
Membungkus dengan Cause #
Mekanismenya sederhana: konstruktor banyak exception — termasuk seluruh buatan kita — menerima Throwable cause setelah pesan, dan meneruskannya ke parent. Method getCause() milik Throwable kemudian mengembalikan objek penyebab, atau null bila tidak ada. Contoh berikut menerjemahkan kegagalan parsing menjadi exception domain tanpa memutus jejaknya:
class FormatUmurSalahException extends RuntimeException {
FormatUmurSalahException(String pesan, Throwable cause) {
super(pesan, cause);
}
}
public class ChainingDasar {
static int bacaUmur(String teks) {
try {
return Integer.parseInt(teks);
} catch (NumberFormatException e) {
throw new FormatUmurSalahException(
"umur harus berupa angka, diterima: \"" + teks + "\"", e);
}
}
public static void main(String[] args) {
try {
System.out.println("umur: " + bacaUmur("dua belas"));
} catch (FormatUmurSalahException e) {
System.out.println("lapisan aplikasi: " + e.getMessage());
Throwable penyebab = e.getCause();
System.out.println("penyebab : " + penyebab.getClass().getSimpleName()
+ " -> " + penyebab.getMessage());
}
}
}Output yang Dihasilkan:
lapisan aplikasi: umur harus berupa angka, diterima: "dua belas"
penyebab : NumberFormatException -> For input string: "dua belas"Mari kita bedah pembungkusannya:
- Baris 3-4 (
FormatUmurSalahException(String pesan, Throwable cause)): konstruktor exception domain menerima dua parameter. Pemanggilansuper(pesan, cause)menitipkan penyebab pada parent —RuntimeExceptionmenyimpannya di field internalcause. - Baris 15 (
throw new FormatUmurSalahException("umur harus berupa angka, ...", e)): parameter kedua adalahe— objekNumberFormatExceptionyang baru saja tertangkap. Inilah momen chaining: exception baru lahir dengan membawa exception lama. - Baris 25-27 (
e.getCause()): pemanggil membaca dua tingkat informasi sekaligus — pesan domain untuk ditampilkan, dan penyebab teknis untuk ditelusuri. Keluaran menunjukkan keduanya:umur harus berupa angka, diterima: "dua belas"di baris pertama,NumberFormatException -> For input string: "dua belas"di baris kedua.
Pembungkusan boleh berlapis lebih dari satu tingkat. Setiap lapisan yang mengubah konteks — dari parsing ke pengaturan, dari pengaturan ke layanan — boleh membungkus sekali lagi, sehingga terbentuk rantai penyebab yang panjang.
Menelusuri Rantai Penyebab #
getCause() mengembalikan satu tingkat penyebab, jadi rantai panjang ditelusuri dengan loop: ambil cause, lalu cause dari cause, sampai bernilai null. Program berikut menyiapkan dua lapisan pembungkusan dan mencetak seluruh rantainya:
class PengaturanRusakException extends RuntimeException {
PengaturanRusakException(String pesan, Throwable cause) {
super(pesan, cause);
}
}
public class RantaiPenyebab {
static int bacaAngka(String teks) {
return Integer.parseInt(teks);
}
static int muatPengaturan(String nama, String nilai) {
try {
return bacaAngka(nilai);
} catch (NumberFormatException e) {
throw new PengaturanRusakException(
"pengaturan '" + nama + "' tidak berupa angka", e);
}
}
public static void main(String[] args) {
try {
System.out.println("umur: " + muatPengaturan("umur", "abc"));
} catch (PengaturanRusakException e) {
System.out.println("rantai kegagalan:");
Throwable t = e;
while (t != null) {
System.out.println("- " + t.getClass().getSimpleName() + ": " + t.getMessage());
t = t.getCause();
}
}
}
}Output yang Dihasilkan:
rantai kegagalan:
- PengaturanRusakException: pengaturan 'umur' tidak berupa angka
- NumberFormatException: For input string: "abc"Loop while di main berjalan dua putaran: putaran pertama mencetak PengaturanRusakException (kegagalan domain), putaran kedua mencetak NumberFormatException (akar teknis), dan putaran ketiga tidak terjadi karena getCause() akhirnya bernilai null. Pola penelusuran seperti inilah yang dipakai logger dan alat monitoring ketika mencetak Caused by: berbaris-baris di balik stack trace utama — setiap baris Caused by: adalah satu simpul rantai cause.
Kapan Membungkus #
Aturan praktisnya: bungkus saat lapisan abstraksi berganti, teruskan saat abstraksinya sama. Method muatPengaturan memanggil bacaAngka — pemanggil lapisan pengaturan tidak perlu tahu detail parsing, jadi kegagalan diterjemahkan ke PengaturanRusakException dengan cause utuh. Sebaliknya, jika method hanya memanggil sesama di lapisan yang sama, melempar ulang exception apa adanya (pola rethrow dari Chapter throw dan throws) lebih jujur daripada membungkus tanpa menambah informasi.
Dua hal terakhir layak dicatat. Pertama, exception yang konstruktornya tidak menerima cause masih bisa di-chain belakangan lewat method initCause(Throwable) — dipanggil sekali sebelum exception dilempar. Kedua, pastikan cause selalu ikut; pembungkusan tanpa cause memotong rantai penelusuran persis di titik yang paling dibutuhkan.
Dengan cause, satu kegagalan bisa berpindah lapisan tanpa kehilangan asal-usulnya. Pertanyaan berikutnya tentang perjalanan, bukan isi: lewat jalur apa exception bergerak dari method ke method sebelum akhirnya tertangkap — atau tidak tertangkap sama sekali? Chapter Exception Propagation memetakan perjalanan itu frame demi frame.
Referensi #
- Class Throwable pada API java.lang
- Oracle: Chained 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.
ChainingDasar.java
Unduh berkasclass FormatUmurSalahException extends RuntimeException {
FormatUmurSalahException(String pesan, Throwable cause) {
super(pesan, cause);
}
}
public class ChainingDasar {
static int bacaUmur(String teks) {
try {
return Integer.parseInt(teks);
} catch (NumberFormatException e) {
throw new FormatUmurSalahException(
"umur harus berupa angka, diterima: \"" + teks + "\"", e);
}
}
public static void main(String[] args) {
try {
System.out.println("umur: " + bacaUmur("dua belas"));
} catch (FormatUmurSalahException e) {
System.out.println("lapisan aplikasi: " + e.getMessage());
Throwable penyebab = e.getCause();
System.out.println("penyebab : " + penyebab.getClass().getSimpleName()
+ " -> " + penyebab.getMessage());
}
}
}
RantaiPenyebab.java
Unduh berkasclass PengaturanRusakException extends RuntimeException {
PengaturanRusakException(String pesan, Throwable cause) {
super(pesan, cause);
}
}
public class RantaiPenyebab {
static int bacaAngka(String teks) {
return Integer.parseInt(teks);
}
static int muatPengaturan(String nama, String nilai) {
try {
return bacaAngka(nilai);
} catch (NumberFormatException e) {
throw new PengaturanRusakException(
"pengaturan '" + nama + "' tidak berupa angka", e);
}
}
public static void main(String[] args) {
try {
System.out.println("umur: " + muatPengaturan("umur", "abc"));
} catch (PengaturanRusakException e) {
System.out.println("rantai kegagalan:");
Throwable t = e;
while (t != null) {
System.out.println("- " + t.getClass().getSimpleName() + ": " + t.getMessage());
t = t.getCause();
}
}
}
}