Memahami hierarki Throwable beserta checked dan unchecked exception, menangani error dengan try/catch/finally, try-with-resources untuk menutup resource secara aman, multi-catch, serta merancang custom exception yang ekspresif untuk domain kalian.

Di episode-episode sebelumnya kita sering menulis throw new IllegalArgumentException ketika ada input tidak valid. Episode ini membedah dunia error handling secara penuh: bagaimana Java merepresentasikan kegagalan, kapan harus menangkap, kapan harus melempar, dan — yang paling penting — bagaimana merancang penanganan error yang tidak menelan kesalahan diam-diam.
Mengapa ini penting? Karena exception yang salah ditangani adalah sumber bug paling halus di produksi: error yang ditelan (catch kosong), exception yang di-lempar saat resource tidak ditutup, atau hierarchy yang terlalu dalam. Error handling yang baik justru membuat kegagalan terlihat jelas di tempat yang tepat.
Semua error di Java hidup dalam satu pohon yang berakar di Throwable:
| Kelas | Sifat | Contoh |
|---|---|---|
Error | Di luar kendali program | OutOfMemoryError, StackOverflowError |
Exception (checked) | Wajib ditangani compiler | IOException, SQLException |
RuntimeException (unchecked) | Tidak wajib, bug program | NullPointerException, IllegalArgumentException |
Pembagian ini adalah keputusan desain penting:
try/catch) atau menyatakan (throws). Dibuat untuk kondisi yang bisa dipulihkan dan di luar kendali program — file tidak ada, jaringan terputus.RuntimeException) — tidak dipaksa compiler. Dibuat untuk bug yang seharusnya tidak terjadi jika kode benar — null tak terduga, indeks di luar batas, argumen tidak valid.import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class BacaFile {
public static void main(String[] args) throws IOException {
String isi = Files.readString(Path.of("data.txt"));
System.out.println(isi);
}
}Hapus throws IOException dan kode tidak akan compile. Itu bukan kelemahan — itu sistem peringatan: compiler memaksa kalian memikirkan kegagalan sebelum terjadi.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) {
try {
String isi = Files.readString(Path.of("data.txt"));
System.out.println(isi);
} catch (IOException e) {
System.out.println("Gagal membaca file: " + e.getMessage());
} finally {
System.out.println("Selalu dijalankan");
}
}
}Tiga blok dengan peran berbeda:
try — kode yang berpotensi gagal.catch — penanganan ketika tipe exception tertentu muncul. finally — selalu dijalankan, baik sukses maupun gagal.finally biasanya dipakai untuk membersihkan resource. Namun sejak Java 7 ada cara yang lebih aman: try-with-resources.
Resource yang mengimplementasikan AutoCloseable bisa ditutup otomatis:
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) {
try (var reader = Files.newBufferedReader(Path.of("data.txt"), StandardCharsets.UTF_8)) {
String baris;
while ((baris = reader.readLine()) != null) {
System.out.println(baris);
}
} catch (IOException e) {
System.out.println("Gagal: " + e.getMessage());
}
}
}Resource dideklarasikan di dalam try (...) dan ditutup otomatis — bahkan saat exception dilempar di tengah proses. Ini menghilangkan kelas bug "lupa menutup koneksi/file" yang menyebabkan kebocoran resource di produksi. BufferedReader, InputStream, Connection SQL — semua AutoCloseable.
Beberapa tipe exception dengan penanganan serupa bisa digabung dengan |:
try {
int hasil = Integer.parseInt(args[0]);
System.out.println(10 / hasil);
} catch (NumberFormatException | ArithmeticException e) {
System.out.println("Input tidak valid: " + e.getMessage());
}Gunakan multi-catch hanya jika penanganannya memang identik — jika berbeda, pisahkan agar masing-masing jelas.
Ketika exception bawaan tidak mengekspresikan domain kalian, buat sendiri:
public class SaldoTidakCukupException extends RuntimeException {
private final double saldo;
private final double dibutuhkan;
public SaldoTidakCukupException(double saldo, double dibutuhkan) {
super("Saldo " + saldo + " tidak cukup untuk " + dibutuhkan);
this.saldo = saldo;
this.dibutuhkan = dibutuhkan;
}
public double kekurangan() {
return dibutuhkan - saldo;
}
}Aturan merancang custom exception:
RuntimeException untuk error yang merepresentasikan kontrak domain (input tidak valid, kondisi bisnis); warisi checked Exception hanya jika pemakai harus menangani.SaldoTidakCukupException lebih baik dari BadRequestException yang generik.try {
kirimPembayaran();
} catch (Exception e) {
// abaikan — jangan pernah begini
}Block catch kosong adalah salah satu bug paling berbahaya: sistem terlihat bekerja padahal sedang rusak, dan tidak ada jejak di log. Minimal, log exception-nya. Idealnya, tangani atau biarkan menyebar ke lapisan yang tepat.
try {
proses();
} catch (Throwable t) {
// salah: menangkap Error juga (OOM, StackOverflow)
}Menangkap Throwable berarti ikut menangkap Error seperti OutOfMemoryError yang seharusnya dibiarkan — menangkapnya justru menyembunyikan kegagalan sistem.
Jangan pakai exception untuk logika normal (misal menunggu NumberFormatException untuk mendeteksi angka tidak valid). Gunakan validasi eksplisit dulu; exception untuk kasus luar biasa, bukan alur sehari-hari.
Tip
Pedoman praktis: biarkan error menangis keras, jangan berbisik. Jika tidak bisa menangani error dengan benar, jangan catch — biarkan menyebar ke handler global yang mencatatnya. Error yang terlihat di log jauh lebih baik daripada error yang "berhasil" disembunyikan.
Warning
Saat menangkap exception lalu melempar yang baru, selalu sertakan exception asli sebagai cause: throw new SaldoTidakCukupException(...) tidak membawa jejak aslinya, sedangkan throw new SaldoTidakCukupException(e, ...) mempertahankan stack trace. Jejak ini sangat berharga saat debugging.
Inti yang harus dibawa pulang:
Throwable → Error (di luar kendali) dan Exception (checked) / RuntimeException (unchecked).finally untuk pembersihan, tetapi try-with-resources lebih aman untuk AutoCloseable.Throwable, dan exception untuk kontrol flow.Di episode 13 selanjutnya kita akan membahas functional programming — lambdas, functional interfaces, Streams dengan map/filter/collect, Optional, dan method references. Sampai jumpa di episode 13!