Seluruh perangkat sudah ada di tangan kita: try-catch, finally, try-with-resources, chaining, dan exception domain milik sendiri. Perangkat tersebut tidak menjawab satu pertanyaan terakhir: ketika exception benar-benar melanda di kode produksi, apa yang sebaiknya dilakukan? Menangkap semuanya di semua tempat membuat kode penuh derau; membiarkan semuanya merambat membuat lapisan atas tenggelam dalam detail teknis. Chapter penutup ini merangkum keputusan tersebut menjadi kerangka sadar — menangani bila bisa pulih, membungkus bila konteks berubah, meneruskan bila tidak tahu — beserta daftar kebiasaan lama yang sebaiknya ditinggalkan.

Tiga Pilihan di Depan Exception #

Setiap blok catch — atau keputusan untuk tidak menulis catch — pada dasarnya memilih satu dari tiga strategi. Kerangka berikut membantu memilih secara sadar:

Menangani tepat di tempat terjadi adalah pilihan terbaik bila dan hanya bila method tersebut sungguh tahu cara memulihkan keadaan. Bila tidak, dua pilihan lainnya justru lebih jujur: membungkus untuk menerjemahkan kegagalan ke bahasa lapisan atas, atau meneruskan apa adanya karena lapisan ini tidak menambah informasi apa pun.

Menangani Saat Bisa Pulih #

Contoh pemulihan yang sehat: pengaturan konfigurasi yang tidak sah cukup diganti nilai bawaan, karena aplikasi tetap berfungsi dan pengguna tetap mendapat perilaku yang masuk akal:

public class TanganiBisaPulih {
    static int bacaBatas(String teks, int bawaan) {
        try {
            return Integer.parseInt(teks);
        } catch (NumberFormatException e) {
            System.out.println("nilai tidak sah '" + teks + "', memakai bawaan " + bawaan);
            return bawaan;
        }
    }

    public static void main(String[] args) {
        System.out.println("batas: " + bacaBatas("25", 10));
        System.out.println("batas: " + bacaBatas("tidak-angka", 10));
    }
}

Output yang Dihasilkan:

text
batas: 25
nilai tidak sah 'tidak-angka', memakai bawaan 10
batas: 10

Kedua pemanggilan menghasilkan nilai yang valid: masukan sah "25" menghasilkan 25, masukan rusak "tidak-angka" digantikan nilai bawaan 10. Penanganan ini layak disebut pemulihan karena dua syaratnya terpenuhi: kegagalan tercatat (baris pemberitahuan tercetak), dan program melanjutkan pekerjaan dengan keadaan yang tetap terdefinisi. Bandingkan dengan CatchKosong di bagian akhir chapter ini — keluarannya mirip, tetapi tanpa catatan dan tanpa kesadaran bahwa kegagalan terjadi.

Gagal Cepat Saat Kontrak Dilanggar #

Strategi kedua bukan penanganan, melainkan pencegahan: fail-fast. Method memeriksa syaratnya di pintu masuk dan langsung melempar dengan pesan yang menyebut pelanggarannya, alih-alih melanjutkan dengan data rusak yang berakibat kegagalan jauh lebih sulit ditelusuri di kemudian hari:

java FailFast.java
public class FailFast {
    static void buatAkun(String nama, int umur) {
        if (nama == null || nama.isBlank()) {
            throw new IllegalArgumentException("nama akun wajib diisi");
        }
        if (umur < 13) {
            throw new IllegalArgumentException("umur minimal 13, diterima: " + umur);
        }
        System.out.println("akun dibuat untuk " + nama);
    }

    public static void main(String[] args) {
        String[] namaUji = {"Budi", ""};
        int[] umurUji = {17, 17};
        for (int i = 0; i < namaUji.length; i++) {
            try {
                buatAkun(namaUji[i], umurUji[i]);
            } catch (IllegalArgumentException e) {
                System.out.println("ditolak sejak awal: " + e.getMessage());
            }
        }
    }
}

Output yang Dihasilkan:

text
akun dibuat untuk Budi
ditolak sejak awal: nama akun wajib diisi

Method buatAkun menolak syarat yang dilanggar sebelum menyentuh logika inti. Pesan kegagalannya menyebut nilai bermasalah (umur minimal 13, diterima: ...), sehingga pemanggil langsung tahu apa yang harus diperbaiki tanpa membuka isi method. Prinsip fail-fast ini pula yang menjadi alasan Objects.requireNonNull(nama, "nama belum diisi") dan kawan-kawannya ada di pustaka standar.

Membungkus dengan Log dan Rethrow #

Strategi ketiga berlaku saat exception melintasi batas lapisan: kegagalan teknis di lapisan penyimpanan perlu tiba di lapisan aplikasi sebagai kegagalan domain, tanpa kehilangan jejak aslinya. Pola log-and-rethrow menggabungkan pencatatan dengan pembungkusan bermakna:

class SimpanGagalException extends RuntimeException {
    SimpanGagalException(String pesan, Throwable cause) {
        super(pesan, cause);
    }
}

public class BungkusDanLanjut {
    static void simpanProfil(String nama) {
        try {
            simpanKePenyimpanan(nama);
            System.out.println("profil " + nama + " tersimpan");
        } catch (IllegalStateException e) {
            System.out.println("log: kegagalan penyimpanan untuk " + nama);
            throw new SimpanGagalException("gagal menyimpan profil " + nama, e);
        }
    }

    static void simpanKePenyimpanan(String nama) {
        throw new IllegalStateException("penyimpanan tidak siap");
    }

    public static void main(String[] args) {
        try {
            simpanProfil("Budi");
        } catch (SimpanGagalException e) {
            System.out.println("lapisan atas menerima: " + e.getMessage());
            System.out.println("penyebab teknis: " + e.getCause().getMessage());
        }
    }
}

Output yang Dihasilkan:

text
log: kegagalan penyimpanan untuk Budi
lapisan atas menerima: gagal menyimpan profil Budi
penyebab teknis: penyimpanan tidak siap

Alurnya menggabungkan tiga teknik dari chapter-chapter sebelumnya: exception teknis (IllegalStateException) ditangkap, konteks dicatat di titik kejadian, lalu dibungkus menjadi SimpanGagalException dengan cause utuh. Lapisan atas menerima bahasa domain (gagal menyimpan profil Budi) dan tetap bisa menelusuri penyebab teknisnya (penyimpanan tidak siap) melalui getCause().

Kebiasaan yang Harus Ditinggalkan #

Sekarang sisi gelapnya. Blok catch kosong adalah anti-pattern paling sering dijumpai pemula — program tampak sehat padahal kegagalan ditelan bulat-bulat:

java CatchKosong.java
public class CatchKosong {
    static int bacaKuota(String teks) {
        try {
            return Integer.parseInt(teks);
        } catch (NumberFormatException e) {
            // blok catch kosong: kegagalan ditelan tanpa jejak apa pun
        }
        return 0;
    }

    public static void main(String[] args) {
        System.out.println("kuota: " + bacaKuota("abc"));
        System.out.println("program seolah tidak pernah gagal");
    }
}

Output yang Dihasilkan:

text
kuota: 0
program seolah tidak pernah gagal

Dua kebiasaan lain turut masuk daftar tinggalkan. Pertama, catch (Exception e) di sembarang tempat: tangkapan selebar itu menyedot kegagalan yang seharusnya diberi tanggapan spesifik, termasuk bug yang justru perlu terlihat. Gunakan tipe sespesifik mungkin; jaring pengaman umum hanya layak di lapisan paling luar aplikasi. Kedua, memakai exception sebagai alur normal — misalnya mengakhiri loop dengan melempar exception alih-alih memakai kondisi berhenti: niat kode menjadi samar dan biaya pelemparan exception lebih mahal daripada pemeriksaan kondisi biasa. Exception adalah sinyal keadaan luar biasa; begitu kejadiannya rutin, ia bukan exception lagi.

Section Exception Handling kini tuntas: dari konsep aliran abnormal, hierarki Throwable, perangkat try-catch-finally dan try-with-resources, hingga strategi menutupnya. Program yang kita tulis selanjutnya akan semakin kaya data — sekumpulan nilai yang perlu disimpan, diurut, dan dicari. Section Collections Framework menyambung persis dari kebutuhan itu.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

BungkusDanLanjut.java

Unduh berkas
java
class SimpanGagalException extends RuntimeException {
    SimpanGagalException(String pesan, Throwable cause) {
        super(pesan, cause);
    }
}

public class BungkusDanLanjut {
    static void simpanProfil(String nama) {
        try {
            simpanKePenyimpanan(nama);
            System.out.println("profil " + nama + " tersimpan");
        } catch (IllegalStateException e) {
            System.out.println("log: kegagalan penyimpanan untuk " + nama);
            throw new SimpanGagalException("gagal menyimpan profil " + nama, e);
        }
    }

    static void simpanKePenyimpanan(String nama) {
        throw new IllegalStateException("penyimpanan tidak siap");
    }

    public static void main(String[] args) {
        try {
            simpanProfil("Budi");
        } catch (SimpanGagalException e) {
            System.out.println("lapisan atas menerima: " + e.getMessage());
            System.out.println("penyebab teknis: " + e.getCause().getMessage());
        }
    }
}

CatchKosong.java

Unduh berkas
java
public class CatchKosong {
    static int bacaKuota(String teks) {
        try {
            return Integer.parseInt(teks);
        } catch (NumberFormatException e) {
            // blok catch kosong: kegagalan ditelan tanpa jejak apa pun
        }
        return 0;
    }

    public static void main(String[] args) {
        System.out.println("kuota: " + bacaKuota("abc"));
        System.out.println("program seolah tidak pernah gagal");
    }
}

FailFast.java

Unduh berkas
java
public class FailFast {
    static void buatAkun(String nama, int umur) {
        if (nama == null || nama.isBlank()) {
            throw new IllegalArgumentException("nama akun wajib diisi");
        }
        if (umur < 13) {
            throw new IllegalArgumentException("umur minimal 13, diterima: " + umur);
        }
        System.out.println("akun dibuat untuk " + nama);
    }

    public static void main(String[] args) {
        String[] namaUji = {"Budi", ""};
        int[] umurUji = {17, 17};
        for (int i = 0; i < namaUji.length; i++) {
            try {
                buatAkun(namaUji[i], umurUji[i]);
            } catch (IllegalArgumentException e) {
                System.out.println("ditolak sejak awal: " + e.getMessage());
            }
        }
    }
}

TanganiBisaPulih.java

Unduh berkas
java
public class TanganiBisaPulih {
    static int bacaBatas(String teks, int bawaan) {
        try {
            return Integer.parseInt(teks);
        } catch (NumberFormatException e) {
            System.out.println("nilai tidak sah '" + teks + "', memakai bawaan " + bawaan);
            return bawaan;
        }
    }

    public static void main(String[] args) {
        System.out.println("batas: " + bacaBatas("25", 10));
        System.out.println("batas: " + bacaBatas("tidak-angka", 10));
    }
}