Saat program Java berhenti mendadak, terminal sering menampilkan belasan baris teks yang terlihat rumit: itulah stack trace, laporan resmi JVM tentang apa yang salah dan di mana kesalahannya terjadi. Membaca laporan tersebut dengan benar adalah keterampilan debugging paling dasar sekaligus paling menghemat waktu, karena jawaban masalah sering sudah tertulis pada frame pertama. Pada Chapter ini kita akan membedah anatomi stack trace, memahami urutan frame, menelusuri rantai Caused by, dan melihat pesan NullPointerException yang informatif sejak Java 14. Semua contoh di sini ditangkap dengan try-catch lalu dicetak secara terstruktur agar hasilnya deterministik dan mudah dibedah.

Anatomi Stack Trace #

Bentuk paling sederhana dari stack trace yang tidak tertangani tampak seperti berikut (diambil dari program yang akan kita jalankan sebentar lagi):

text
Exception in thread "main" java.lang.IllegalStateException: Stok habis untuk pesanan 42
	at BedahStack.validasiStok(BedahStack.java:27)
	at BedahStack.prosesPesanan(BedahStack.java:22)
	at BedahStack.main(BedahStack.java:4)

Baris pertama memuat empat informasi kunci yang perlu kita baca berurutan:

  • Exception in thread "main" — exception terjadi di thread bernama main.
  • java.lang.IllegalStateException — jenis exception yang terlempar.
  • Stok habis untuk pesanan 42 — pesan yang diberikan saat exception dibuat.
  • Baris at ... — frame: pasangan Class.method plus nama berkas dan nomor baris tempat eksekusi berada ketika exception dilempar atau dipanggil.

Urutan frame bercerita tentang arah pembacaan. Frame paling atas adalah titik tempat exception dilempar; frame di bawahnya adalah pemanggil method tersebut, lalu pemanggil dari pemanggil, sampai main. Membaca dari atas ke bawah berarti menelusuri dari sumber masalah menuju pintu masuk program — kebalikan dari urutan eksekusi.

Strategi Membaca Frame #

Aturan praktisnya sederhana: cari frame paling atas yang berada di kode milik kita sendiri. Frame dari pustaka standar seperti java.util atau java.lang.reflect memang muncul di trace, tetapi masalahnya hampir selalu berasal dari nilai yang kita oper, bukan dari pustakanya. Jika frame paling atas milik pustaka, turun satu frame ke bawah dan periksa baris kode kita yang memanggil pustaka tersebut.

Nomor baris pada frame menunjuk tepat ke pernyataan yang aktif saat exception terjadi. Informasi ini berasal dari tabel baris (line number table) yang disisipkan compiler javac ke dalam class file, sehingga JVM tahu posisi sumber asli untuk setiap instruksi bytecode. Tanpa informasi itu, trace hanya menampilkan (Unknown Source) dan kita kehilangan petunjuk lokasi.

Menangkap dan Membedah Frame #

Daripada membiarkan program berhenti, kita dapat menangkap exception lalu memeriksa bagiannya lewat method Throwable. Program berikut melempar exception pada kedalaman tiga method, menangkapnya, lalu mencetak jenis, pesan, dan dua frame teratas:

java BedahStack.java
public class BedahStack {
    public static void main(String[] args) {
        try {
            prosesPesanan(42);
        } catch (IllegalStateException e) {
            // Tangkap, lalu bedah bagian demi bagian secara deterministik
            System.out.println("Jenis exception : " + e.getClass().getName());
            System.out.println("Pesan           : " + e.getMessage());

            StackTraceElement frame0 = e.getStackTrace()[0];
            System.out.println("Frame paling atas: " + frame0.getMethodName()
                    + " di baris " + frame0.getLineNumber());

            StackTraceElement frame1 = e.getStackTrace()[1];
            System.out.println("Frame berikutnya: " + frame1.getMethodName()
                    + " di baris " + frame1.getLineNumber());
        }
        System.out.println("Program tetap berjalan sampai selesai.");
    }

    static void prosesPesanan(int id) {
        validasiStok(id);
    }

    static void validasiStok(int id) {
        // Sumber masalah sesungguhnya ada di sini
        throw new IllegalStateException("Stok habis untuk pesanan " + id);
    }
}

Output yang Dihasilkan:

text
Jenis exception : java.lang.IllegalStateException
Pesan           : Stok habis untuk pesanan 42
Frame paling atas: validasiStok di baris 27
Frame berikutnya: prosesPesanan di baris 22
Program tetap berjalan sampai selesai.

Mari kita telaah bagian penting dari program di atas:

  1. Baris 3-5 (prosesPesanan(42) di dalam try): memanggil method berlapis untuk mensimulasikan kedalaman tiga frame; main → prosesPesanan → validasiStok.
  2. Baris 27 (throw new IllegalStateException(...)): sumber masalah sebenarnya. Frame paling atas pada stack trace akan menunjuk ke baris ini.
  3. Baris 7-8 (e.getClass().getName() dan e.getMessage()): mengambil nama class exception dan pesannya — dua informasi pertama pada baris Exception in thread ....
  4. Baris 10-16 (e.getStackTrace()[0] dan [1]): membaca frame sebagai objek StackTraceElement. Indeks 0 adalah frame teratas (validasiStok), indeks 1 pemanggilnya (prosesPesanan).
  5. Baris 18: bukti bahwa exception yang tertangkap tidak menghentikan program — eksekusi berlanjut setelah blok catch.

Rantai Penyebab dengan Caused by #

Aplikasi nyata sering membungkus ulang exception di setiap lapisan agar pesannya semakin berkonteks. Praktik baiknya tetap menyimpan penyebab asli lewat constructor Throwable(pesan, cause), sehingga trace memuat bagian Caused by: di bagian bawah. Program berikut memperagakan rantai dua lapis dan membaca penyebabnya:

public class RantaiPenyebab {
    public static void main(String[] args) {
        try {
            muatKonfigurasi();
        } catch (IllegalStateException e) {
            System.out.println("Jenis   : " + e.getClass().getSimpleName());
            System.out.println("Pesan   : " + e.getMessage());

            Throwable akar = e.getCause();
            System.out.println("Penyebab: " + akar.getClass().getSimpleName()
                    + " - " + akar.getMessage());

            StackTraceElement frameAkar = akar.getStackTrace()[0];
            System.out.println("Frame penyebab: " + frameAkar.getMethodName()
                    + " di baris " + frameAkar.getLineNumber());
        }
    }

    static void muatKonfigurasi() {
        try {
            bacaBerkas();
        } catch (IllegalArgumentException akar) {
            // Bungkus ulang dengan konteks, tapi simpan penyebab aslinya
            throw new IllegalStateException("Gagal memuat konfigurasi", akar);
        }
    }

    static void bacaBerkas() {
        throw new IllegalArgumentException("Baris ke-3 bukan angka");
    }
}

Output yang Dihasilkan:

text
Jenis   : IllegalStateException
Pesan   : Gagal memuat konfigurasi
Penyebab: IllegalArgumentException - Baris ke-3 bukan angka
Frame penyebab: bacaBerkas di baris 29

Poin penting dari contoh tersebut: method getCause() mengembalikan exception asli yang tersimpan, dan frame pertama penyebab itulah (bacaBerkas) titik awal investigasi kita. Saat membaca trace panjang di produksi, lompat langsung ke Caused by terakhir karena lapisan pembungkus biasanya hanya mengulang konteks.

Pesan NPE yang Membantu #

Sejak Java 14, compiler menjelaskan NullPointerException (NPE) secara rinci lewat Helpful NullPointerExceptions: pesannya menyebut operasi yang gagal dan variabel yang bernilai null. Program kecil berikut memicu NPE pada akses field, lalu mencetak pesannya:

java PesanNpe.java
class Pelanggan {
    String nama;
}

public class PesanNpe {
    static Pelanggan p;   // field: namanya selalu tercatat di class file

    public static void main(String[] args) {
        p = null;
        try {
            System.out.println(p.nama.length());
        } catch (NullPointerException e) {
            // Helpful NullPointerExceptions (aktif sejak Java 14)
            System.out.println(e.getMessage());
        }
    }
}

Output yang Dihasilkan:

text
Cannot read field "nama" because "PesanNpe.p" is null

Pesan Cannot read field "nama" because "PesanNpe.p" is null menghemat tebakan: kita langsung tahu field p yang belum diisi, bukan nama. Perhatikan juga bahwa contoh memakai field, bukan variabel lokal, karena nama field selalu tercatat di class file sementara nama lokal hanya muncul bila class dikompilasi dengan opsi debug penuh.

Dengan kemampuan membaca anatomi, urutan frame, rantai penyebab, dan pesan NPE, kita sudah memiliki dasar diagnosis tanpa alat tambahan. Namun membaca jejak pasca-kejadian tetap terbatas: kita tidak melihat nilai variabel di sekitar baris bermasalah. Chapter berikutnya memperkenalkan IDE debugger yang menghentikan eksekusi tepat di titik kejadian sehingga kita bisa memeriksa program secara langsung.

Referensi #

Berkas Sumber

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

BedahStack.java

Unduh berkas
java
public class BedahStack {
    public static void main(String[] args) {
        try {
            prosesPesanan(42);
        } catch (IllegalStateException e) {
            // Tangkap, lalu bedah bagian demi bagian secara deterministik
            System.out.println("Jenis exception : " + e.getClass().getName());
            System.out.println("Pesan           : " + e.getMessage());

            StackTraceElement frame0 = e.getStackTrace()[0];
            System.out.println("Frame paling atas: " + frame0.getMethodName()
                    + " di baris " + frame0.getLineNumber());

            StackTraceElement frame1 = e.getStackTrace()[1];
            System.out.println("Frame berikutnya: " + frame1.getMethodName()
                    + " di baris " + frame1.getLineNumber());
        }
        System.out.println("Program tetap berjalan sampai selesai.");
    }

    static void prosesPesanan(int id) {
        validasiStok(id);
    }

    static void validasiStok(int id) {
        // Sumber masalah sesungguhnya ada di sini
        throw new IllegalStateException("Stok habis untuk pesanan " + id);
    }
}

PesanNpe.java

Unduh berkas
java
class Pelanggan {
    String nama;
}

public class PesanNpe {
    static Pelanggan p;   // field: namanya selalu tercatat di class file

    public static void main(String[] args) {
        p = null;
        try {
            System.out.println(p.nama.length());
        } catch (NullPointerException e) {
            // Helpful NullPointerExceptions (aktif sejak Java 14)
            System.out.println(e.getMessage());
        }
    }
}

RantaiPenyebab.java

Unduh berkas
java
public class RantaiPenyebab {
    public static void main(String[] args) {
        try {
            muatKonfigurasi();
        } catch (IllegalStateException e) {
            System.out.println("Jenis   : " + e.getClass().getSimpleName());
            System.out.println("Pesan   : " + e.getMessage());

            Throwable akar = e.getCause();
            System.out.println("Penyebab: " + akar.getClass().getSimpleName()
                    + " - " + akar.getMessage());

            StackTraceElement frameAkar = akar.getStackTrace()[0];
            System.out.println("Frame penyebab: " + frameAkar.getMethodName()
                    + " di baris " + frameAkar.getLineNumber());
        }
    }

    static void muatKonfigurasi() {
        try {
            bacaBerkas();
        } catch (IllegalArgumentException akar) {
            // Bungkus ulang dengan konteks, tapi simpan penyebab aslinya
            throw new IllegalStateException("Gagal memuat konfigurasi", akar);
        }
    }

    static void bacaBerkas() {
        throw new IllegalArgumentException("Baris ke-3 bukan angka");
    }
}