Tampilan
Kenapa penting
Sejak M02 kamu sudah sering menulis throw new IllegalArgumentException(...) dengan janji "mekanisme exception dibahas di M03". Inilah saatnya. Di backend, kegagalan adalah hal biasa: input pengguna salah format, stok habis, file tidak ada, database tidak merespons. Yang membedakan aplikasi rapuh dan aplikasi tangguh adalah cara menangani kegagalan itu: apakah satu baris data rusak menghentikan seluruh proses impor, apakah pengguna mendapat pesan yang jelas, dan apakah tim bisa menemukan penyebabnya dari log. Semua itu dimulai dari memahami exception dan membaca stack trace.
Konsep inti
Apa yang terjadi saat exception dilempar
Exception adalah object yang mewakili kejadian tidak normal. Saat sebuah exception dilempar, eksekusi method langsung berhenti — baris berikutnya tidak dijalankan — dan exception itu merambat naik ke method pemanggil, lalu ke pemanggilnya lagi, sampai ada yang menangkapnya. Kalau tidak ada yang menangkap sampai main, program berhenti dan JVM mencetak stack trace.
java
public class StackTraceDemo {
static int parseQuantity(String raw) {
return Integer.parseInt(raw);
}
static long subtotal(long price, String rawQuantity) {
int quantity = parseQuantity(rawQuantity);
System.out.println("jumlah = " + quantity);
return price * quantity;
}
public static void main(String[] args) {
System.out.println("Mulai");
System.out.println(subtotal(25_000, "2"));
System.out.println(subtotal(25_000, "dua"));
System.out.println("Selesai");
}
}Output:
text
Mulai
jumlah = 2
50000
Exception in thread "main" java.lang.NumberFormatException: For input string: "dua"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:565)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at StackTraceDemo.parseQuantity(StackTraceDemo.java:3)
at StackTraceDemo.subtotal(StackTraceDemo.java:7)
at StackTraceDemo.main(StackTraceDemo.java:15)Pada panggilan kedua, jumlah = ... dan Selesai tidak pernah tercetak: exception memotong alur di parseQuantity, melewati subtotal, lalu keluar dari main.
Membaca stack trace
Stack trace dibaca seperti ini:
- Baris pertama — jenis exception dan pesannya:
NumberFormatException: For input string: "dua". Sering kali ini saja sudah cukup menjelaskan masalahnya. - Baris
at ...— daftar method yang sedang berjalan, dari yang paling dalam (tempat exception dibuat) di atas sampaimaindi bawah. Setiap baris menyebut class, method, file, dan nomor baris. - Cari baris pertama milik kodemu. Tiga baris teratas di atas ada di dalam JDK (
java.base/...). BarisStackTraceDemo.parseQuantity(StackTraceDemo.java:3)adalah titik di mana kodemu memberikan input yang salah.
Exception juga bisa membawa penyebab (cause). Bila sebuah exception dibungkus exception lain, stack trace punya bagian Caused by:. Misalnya method yang menangkap NumberFormatException lalu melempar new IllegalStateException("Gagal memproses pesanan INV-7", e) — dikompilasi dengan javac lalu dijalankan dengan java:
text
Exception in thread "main" java.lang.IllegalStateException: Gagal memproses pesanan INV-7
at CausedBy.processOrder(CausedBy.java:10)
at CausedBy.main(CausedBy.java:15)
Caused by: java.lang.NumberFormatException: For input string: "2x"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:565)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at CausedBy.parseQuantity(CausedBy.java:3)
at CausedBy.processOrder(CausedBy.java:8)
... 1 moreBagian atas menjelaskan konteks bisnis ("pesanan INV-7"), bagian Caused by: menunjukkan akar teknisnya. Di log aplikasi Spring, Caused by: paling bawah biasanya akar masalah sebenarnya. ... 1 more berarti sisa baris sama dengan stack trace di atasnya.
Untuk NullPointerException, Java modern (sejak JEP 358) menyebutkan apa yang bernilai null:
java
record Customer(String name, String email) {}
record Order(String id, Customer customer) {}
public class HelpfulNpe {
public static void main(String[] args) {
Order guestOrder = new Order("INV-9", null);
String email = guestOrder.customer().email();
System.out.println(email);
}
}text
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Customer.email()" because the return value of "Order.customer()" is null
at HelpfulNpe.main(HelpfulNpe.java:8)Pesannya langsung menunjuk biang keladinya: customer() mengembalikan null untuk pesanan tamu.
try, catch, dan finally
Untuk menangani exception, bungkus kode yang berisiko dengan try, lalu tulis catch untuk jenis exception yang ingin kamu tangani. Blok finally (opsional) selalu dijalankan saat keluar dari try — baik normal, lewat return, maupun karena exception.
java
public class TryCatchDemo {
static int parseQuantity(String raw) {
try {
int quantity = Integer.parseInt(raw.strip());
System.out.println(" parse berhasil");
return quantity;
} catch (NumberFormatException e) {
System.out.println(" ditangkap: " + e.getMessage());
return 0;
} finally {
System.out.println(" finally untuk \"" + raw + "\"");
}
}
public static void main(String[] args) {
System.out.println("Hasil: " + parseQuantity(" 3 "));
System.out.println("Hasil: " + parseQuantity("tiga"));
}
}Output:
text
parse berhasil
finally untuk " 3 "
Hasil: 3
ditangkap: For input string: "tiga"
finally untuk "tiga"
Hasil: 0Perhatikan bahwa finally tetap jalan walaupun try sudah return. Itu sebabnya finally dipakai untuk pekerjaan "bersih-bersih" — melepas kunci, menutup koneksi. Untuk menutup resource ada cara yang lebih ringkas dan aman, yaitu try-with-resources (dibahas di bab Try-with-Resources).
Beberapa catch dan multi-catch
Satu try boleh punya beberapa catch. Java memeriksanya dari atas ke bawah dan memakai yang pertama cocok, jadi urutkan dari yang paling spesifik. Bila urutannya terbalik, compiler menolak:
text
WrongOrder.java:7: error: exception NumberFormatException has already been caught
} catch (NumberFormatException e) {
^
1 errorBila beberapa jenis exception ditangani dengan cara yang sama, gabungkan dengan multi-catch |:
java
public class MultiCatch {
static String pricePerItem(String rawTotal, String rawCount) {
try {
int total = Integer.parseInt(rawTotal);
int count = Integer.parseInt(rawCount);
return "Rp" + (total / count);
} catch (NumberFormatException | ArithmeticException e) {
return "tidak bisa dihitung (" + e.getClass().getSimpleName() + ": " + e.getMessage() + ")";
}
}
public static void main(String[] args) {
System.out.println(pricePerItem("90000", "3"));
System.out.println(pricePerItem("90000", "nol"));
System.out.println(pricePerItem("90000", "0"));
}
}Output:
text
Rp30000
tidak bisa dihitung (NumberFormatException: For input string: "nol")
tidak bisa dihitung (ArithmeticException: / by zero)throw dan throws
throwadalah statement untuk melempar exception:throw new IllegalStateException("Stok kurang"). Ini yang sudah kamu pakai sejak M02.throwsditulis di deklarasi method untuk mengumumkan bahwa method itu bisa melempar exception tertentu, dan pemanggilnya harus siap menanganinya.
java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ThrowsDemo {
static void checkStock(int requested, int available) {
if (requested > available) {
throw new IllegalStateException("Stok kurang: diminta " + requested + ", tersedia " + available);
}
}
static String readTemplate(String path) throws IOException {
return Files.readString(Path.of(path));
}
public static void main(String[] args) {
try {
checkStock(3, 10);
System.out.println("stok cukup");
checkStock(12, 10);
} catch (IllegalStateException e) {
System.out.println("Ditolak: " + e.getMessage());
}
try {
readTemplate("template-email-tidak-ada.txt");
} catch (IOException e) {
System.out.println("Gagal membaca: " + e);
}
}
}Output:
text
stok cukup
Ditolak: Stok kurang: diminta 12, tersedia 10
Gagal membaca: java.nio.file.NoSuchFileException: template-email-tidak-ada.txtKenapa readTemplate wajib menulis throws IOException sedangkan checkStock tidak perlu menulis apa pun? Jawabannya ada di hierarki exception — dan dibahas tuntas di bab Checked vs Unchecked.
Hierarki Throwable
Semua yang bisa dilempar adalah turunan Throwable:
text
Throwable
├── Error masalah serius JVM — jangan ditangkap
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception "checked" — wajib ditangani atau diumumkan
├── IOException
│ └── NoSuchFileException
└── RuntimeException "unchecked" — tidak wajib diumumkan
├── IllegalArgumentException
│ └── NumberFormatException
├── IllegalStateException
├── NullPointerException
├── ArithmeticException
└── IndexOutOfBoundsExceptionKarena catch menangkap tipe beserta semua turunannya, catch (IllegalArgumentException e) juga menangkap NumberFormatException. Fakta ini berguna, tetapi juga berbahaya: catch (Exception e) menangkap hampir segalanya — termasuk bug seperti NullPointerException yang seharusnya diperbaiki, bukan disembunyikan.
Contoh di dunia backend
Fitur impor pesanan dari file CSV: setiap baris berisi sku;jumlah;harga. Satu baris rusak tidak boleh menggagalkan seluruh impor. Baris yang gagal dilewati dan dicatat beserta nomornya, supaya admin bisa memperbaiki file:
java
import java.util.ArrayList;
import java.util.List;
record OrderLine(String sku, int quantity, long unitPrice) {
long subtotal() {
return quantity * unitPrice;
}
}
public class OrderImport {
static OrderLine parseLine(String line) {
String[] parts = line.split(";");
if (parts.length != 3) {
throw new IllegalArgumentException("butuh 3 kolom, ada " + parts.length);
}
int quantity = Integer.parseInt(parts[1].strip());
long unitPrice = Long.parseLong(parts[2].strip());
if (quantity < 1) {
throw new IllegalArgumentException("jumlah harus >= 1: " + quantity);
}
return new OrderLine(parts[0].strip(), quantity, unitPrice);
}
public static void main(String[] args) {
List<String> rows = List.of(
"SKU-001;2;25000",
"SKU-002;dua;10000",
"SKU-003;1",
"SKU-004;0;5000",
"SKU-005;3;12000");
List<OrderLine> accepted = new ArrayList<>();
List<String> errors = new ArrayList<>();
for (int i = 0; i < rows.size(); i++) {
try {
accepted.add(parseLine(rows.get(i)));
} catch (IllegalArgumentException e) { // juga menangkap NumberFormatException
errors.add("baris " + (i + 1) + ": " + e.getMessage());
}
}
long total = 0;
for (OrderLine line : accepted) {
total += line.subtotal();
}
System.out.println("Diterima " + accepted.size() + " baris, total Rp" + total);
System.out.println("Ditolak " + errors.size() + " baris:");
for (String error : errors) {
System.out.println(" " + error);
}
}
}Output:
text
Diterima 2 baris, total Rp86000
Ditolak 3 baris:
baris 2: For input string: "dua"
baris 3: butuh 3 kolom, ada 2
baris 4: jumlah harus >= 1: 0Beberapa keputusan desain di sini: try/catch diletakkan di dalam loop sehingga satu kegagalan hanya membatalkan satu baris; yang ditangkap hanya IllegalArgumentException (input buruk), bukan Exception — bug lain seperti NullPointerException tetap terlihat; dan setiap error dicatat dengan konteks (nomor baris), bukan dibuang.
Pertanyaan interview
1. Apa beda Error dan Exception?
Keduanya turunan Throwable. Error menandakan masalah serius di JVM atau lingkungan (mis. OutOfMemoryError, StackOverflowError) yang umumnya tidak bisa dipulihkan aplikasi, jadi tidak ditangkap. Exception mewakili kondisi yang masih masuk akal untuk ditangani aplikasi — input salah, file tidak ada, layanan lain gagal.
2. Apa fungsi finally, dan kapan ia tidak dijalankan?
finally dijalankan setiap kali eksekusi keluar dari blok try — selesai normal, lewat return, atau karena exception — sehingga cocok untuk membersihkan resource. Ia tidak jalan bila JVM berhenti total, misalnya System.exit() atau proses dimatikan. Hindari return di dalam finally karena menimpa nilai kembali dan menelan exception dari try.
3. Kenapa catch (Exception e) {} dianggap berbahaya?
Ia menangkap hampir semua exception, termasuk bug yang seharusnya diperbaiki, lalu membuangnya tanpa jejak. Program melanjutkan alur seolah sukses, data bisa rusak, dan tim tidak punya log untuk menelusuri masalah. Tangkap jenis yang spesifik dan memang bisa kamu tangani; sisanya biarkan merambat ke penanganan terpusat.
Jebakan umum
catchkosong (menelan exception). Minimal catat ke log beserta object exception-nya, atau lempar ulang.catch (Exception e)di mana-mana. Bug ikut tersembunyi. Tangkap jenis spesifik; penangkapan luas hanya di satu tempat terpusat (mis. handler error global di aplikasi web).- Menangkap
NullPointerExceptionalih-alih memperbaiki penyebabnull-nya. - Mencetak
e.getMessage()saja. Pesan bisanulldan stack trace hilang. Di log, sertakan object exception-nya. returndi dalamfinally. Nilai kembali daritrytertimpa dan exception yang sedang merambat ikut hilang.- Membaca stack trace dari baris paling bawah. Mulai dari pesan di baris pertama dan baris pertama milik kodemu.
Ringkasan
- Exception menghentikan method dan merambat naik ke pemanggil sampai ditangkap; tanpa penangkap, program berhenti.
- Baca stack trace dari pesan di baris pertama, lalu cari baris pertama milik kodemu;
Caused by:menunjukkan akar masalah. try/catchmenangani exception;finallyselalu jalan;catchdiurutkan dari spesifik ke umum; multi-catch|untuk penanganan yang sama.throwmelempar exception;throwsmengumumkan exception yang mungkin keluar dari method.Throwableterbagi menjadiError(jangan ditangkap) danException;RuntimeExceptionadalah cabang khusus.- Jangan menelan exception dan jangan menangkap terlalu luas.