Exception bawaan seperti IllegalStateException atau IllegalArgumentException bicara dalam bahasa yang sangat umum: “keadaan tidak sah”, “argumen tidak sah”. Aplikasi nyata memiliki kosakata kegagalan yang jauh lebih kaya — saldo tidak cukup, nama pelanggan ganda, kuota terlampaui — dan pesan kegagalan yang menyebut nama domainnya langsung lebih mudah dibaca, dicatat, dan ditangani secara terpisah. Java tidak membatasi kita pada pustaka bawaan: membuat custom exception semudah membuat class yang mewarisi RuntimeException atau Exception. Pada Chapter ini kita buat dua jenis exception domain, satu unchecked dan satu checked, lengkap dengan konstruktornya.
Mengapa Exception Milik Sendiri #
Manfaatnya konkret. Pertama, ketepatan penangkapan: blok catch (SaldoTidakCukupException e) menangkap tepat satu makna bisnis, bukan campuran semua keadaan tidak sah. Kedua, keterbacaan log: baris log SaldoTidakCukupException: saldo 20000 tidak cukup untuk penarikan 50000 langsung bisa dipahami tanpa membuka kode. Ketiga, kontrak yang hidup di tipe: pengguna method tidak perlu membaca dokumentasi untuk tahu jenis kegagalan yang mungkin — tanda tangan method dan hierarki class-nya sudah bercerita.
Membuatnya mengikuti pola pewarisan biasa. Pilihan parent menentukan karakter exception yang kita ciptakan: turunkan dari RuntimeException untuk kegagalan ala bug atau pelanggaran aturan domain yang tidak wajib ditangani setiap pemanggil, atau turunkan dari Exception untuk kegagalan prosedural yang pemanggil wajib sadari. Konvensi penamaannya lugas: nama yang menjelaskan kegagalan, diakhiri kata Exception.
Versi Unchecked untuk Aturan Domain #
Kita mulai dari exception untuk mesin penarikan rekening. Karena pemanggil method tarik umumnya sudah tahu risikonya dan kegagalan ini bagian dari alur domain, kita pilih cabang unchecked. Berikut definisi class-nya — perhatikan bahwa isinya hanya konstruktor:
public class SaldoTidakCukupException extends RuntimeException {
public SaldoTidakCukupException(String pesan) {
super(pesan);
}
public SaldoTidakCukupException(String pesan, Throwable cause) {
super(pesan, cause);
}
}Konstruktor hanya meneruskan pesan (dan opsional cause) ke konstruktor parent. Parent RuntimeException yang menyimpan keduanya dan menyediakan getMessage() serta getCause(). Konstruktor kedua tampak belum terpakai di contoh ini — konstruktor bentuk itu menjadi kunci pada Chapter Exception Chaining. Sekarang kita pakai exception tersebut pada aplikasi rekening sederhana:
public class AplikasiRekening {
static int saldo = 50000;
static void tarik(int jumlah) {
if (jumlah > saldo) {
throw new SaldoTidakCukupException(
"saldo " + saldo + " tidak cukup untuk penarikan " + jumlah);
}
saldo -= jumlah;
}
public static void main(String[] args) {
try {
tarik(30000);
System.out.println("penarikan berhasil, saldo: " + saldo);
tarik(50000);
System.out.println("penarikan berhasil, saldo: " + saldo);
} catch (SaldoTidakCukupException e) {
System.out.println("ditolak: " + e.getMessage());
}
}
}Output yang Dihasilkan:
penarikan berhasil, saldo: 20000
ditolak: saldo 20000 tidak cukup untuk penarikan 50000Mari kita bedah alurnya:
- Baris 7-9 (
throw new SaldoTidakCukupException(...)): methodtarikmelempar exception domain dengan pesan yang memuat kedua angka penting — saldo saat ini dan jumlah yang diminta. Tanpa custom exception, pilihan yang tersedia hanyaIllegalStateExceptionyang tidak membedakan makna kegagalan. - Baris 17 (
catch (SaldoTidakCukupException e)): blok ini menangkap hanya kegagalan kekurangan saldo. Jikatarikkelak melempar jenis lain, blok ini tidak akan menelan kegagalan tersebut secara diam-diam. - Keluaran program: penarikan pertama (
30000) berhasil dan saldo menjadi20000; penarikan kedua (50000) ditolak dengan pesan yang menyebut angka20000— nilai saldo setelah penarikan pertama, karena variabelsaldosudah berubah.
Versi Checked untuk Prosedur Wajib Sadar #
Untuk contoh checked, kita buat exception yang dipakai prosedur validasi data pelanggan. Berbeda dengan penarikan saldo, kegagalan validasi adalah hal yang setiap pemanggil wajib tangani secara eksplisit — data yang tidak lolos tidak boleh diam-diam dianggap sah:
public class ValidasiGagalException extends Exception {
public ValidasiGagalException(String pesan) {
super(pesan);
}
}Perubahan kuncinya hanya satu kata: parent-nya Exception, bukan RuntimeException. Efeknya langsung terasa pada method pemakainya — klausa throws menjadi wajib:
public class AplikasiValidasi {
static void validasiNama(String nama) throws ValidasiGagalException {
if (nama == null || nama.isBlank()) {
throw new ValidasiGagalException("nama wajib diisi");
}
if (!nama.matches("[a-zA-Z ]+")) {
throw new ValidasiGagalException("nama hanya boleh huruf: " + nama);
}
}
public static void main(String[] args) {
String[] kandidat = {"Budi Santoso", "Dewi123", ""};
for (String nama : kandidat) {
try {
validasiNama(nama);
System.out.println("lolos: " + nama);
} catch (ValidasiGagalException e) {
System.out.println("gagal: " + e.getMessage());
}
}
}
}Output yang Dihasilkan:
lolos: Budi Santoso
gagal: nama hanya boleh huruf: Dewi123
gagal: nama wajib diisiMethod validasiNama menolak Dewi123 karena mengandung angka, dan menolak string kosong karena dianggap sama dengan tidak mengisi. Tanpa klausa throws ValidasiGagalException pada baris tanda tangan, kompilasi gagal dengan pesan unreported exception yang sudah kita kenal dari Chapter Checked vs Unchecked Exceptions — bukti bahwa exception buatan kita menikmati seluruh perlakuan compiler yang sama dengan IOException.
Dua keputusan yang tersisa kini berupa soal komposisi: bagaimana saat kegagalan IOException di lapisan penyimpanan perlu dilaporkan ke lapisan aplikasi sebagai SaldoTidakCukupException atau exception domain lain — tanpa kehilangan jejak kegagalan aslinya? Jawabannya adalah membungkus dengan cause, topik Chapter Exception Chaining berikutnya.
Referensi #
- Class Throwable pada API java.lang
- Class RuntimeException pada API java.lang
- Oracle: Creating Exception Classes di dev.java
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
AplikasiRekening.java
Unduh berkaspublic class AplikasiRekening {
static int saldo = 50000;
static void tarik(int jumlah) {
if (jumlah > saldo) {
throw new SaldoTidakCukupException(
"saldo " + saldo + " tidak cukup untuk penarikan " + jumlah);
}
saldo -= jumlah;
}
public static void main(String[] args) {
try {
tarik(30000);
System.out.println("penarikan berhasil, saldo: " + saldo);
tarik(50000);
System.out.println("penarikan berhasil, saldo: " + saldo);
} catch (SaldoTidakCukupException e) {
System.out.println("ditolak: " + e.getMessage());
}
}
}
AplikasiValidasi.java
Unduh berkaspublic class AplikasiValidasi {
static void validasiNama(String nama) throws ValidasiGagalException {
if (nama == null || nama.isBlank()) {
throw new ValidasiGagalException("nama wajib diisi");
}
if (!nama.matches("[a-zA-Z ]+")) {
throw new ValidasiGagalException("nama hanya boleh huruf: " + nama);
}
}
public static void main(String[] args) {
String[] kandidat = {"Budi Santoso", "Dewi123", ""};
for (String nama : kandidat) {
try {
validasiNama(nama);
System.out.println("lolos: " + nama);
} catch (ValidasiGagalException e) {
System.out.println("gagal: " + e.getMessage());
}
}
}
}
SaldoTidakCukupException.java
Unduh berkaspublic class SaldoTidakCukupException extends RuntimeException {
public SaldoTidakCukupException(String pesan) {
super(pesan);
}
public SaldoTidakCukupException(String pesan, Throwable cause) {
super(pesan, cause);
}
}
ValidasiGagalException.java
Unduh berkaspublic class ValidasiGagalException extends Exception {
public ValidasiGagalException(String pesan) {
super(pesan);
}
}