Ketika PHP berhenti mendadak, terminal sering dipenuhi blok teks panjang yang berawalan Fatal error: Uncaught. Blok tersebut adalah stack trace: rekaman rantai panggilan function yang berujung pada titik kegagalan. Trace yang sama juga bisa kita minta dari exception yang sudah ditangkap, sehingga jejak eksekusi dapat dianalisis tanpa menghentikan program. Membaca trace dengan cepat dan teliti adalah keterampilan debugging yang paling sering terpakai, jauh sebelum kita menyentuh debugger interaktif. Chapter ini membedah anatomi trace asli PHP baris demi baris, melatih kita menelusuri frame yang melintasi beberapa berkas, dan menunjukkan cara mencetak trace langsung dari dalam kode.

Anatomi Trace dari Exception yang Tidak Ditangkap #

Kita mulai dari kasus yang paling sering dijumpai: exception lolos tanpa penangkap, lalu PHP mencetak fatal error beserta trace lengkapnya. Contoh berikut menyiapkan rantai panggilan dua tingkat — prosesKeranjang() memanggil hitungDiskon() — dan sengaja mengirim nilai diskon yang tidak valid. Hasilnya adalah output error nyata, bukan ilustrasi:

php
<?php
function hitungDiskon(float $harga, float $persen): float
{
    if ($persen < 0 || $persen > 100) {
        throw new InvalidArgumentException(
            "Persentase diskon harus di antara 0 dan 100, diterima: $persen"
        );
    }

    return $harga - ($harga * $persen / 100);
}

function prosesKeranjang(array $item): float
{
    return hitungDiskon($item["harga"], $item["diskon"]);
}

$keranjang = ["nama" => "Kopi Gayo", "harga" => 85000.0, "diskon" => 150.0];

echo "Memproses keranjang...\n";
echo "Total setelah diskon: " . prosesKeranjang($keranjang) . "\n";

Output yang Dihasilkan:

text
Memproses keranjang...

Fatal error: Uncaught InvalidArgumentException: Persentase diskon harus di antara 0 dan 100, diterima: 150 in /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php:5
Stack trace:
#0 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(15): hitungDiskon(85000.0, 150.0)
#1 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(21): prosesKeranjang(Array)
#2 {main}
  thrown in /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php on line 5

Mari kita bedah bagian-bagian penting dari output fatal di atas:

  1. Baris utama (Fatal error: Uncaught InvalidArgumentException: ...): memuat class exception, pesannya, berkas tempat throw berada, dan nomor baris throw tersebut (...:5 menunjuk baris 5, yaitu pernyataan throw di dalam hitungDiskon()).
  2. Frame #0: pemanggilan paling baru menjelang kegagalan — hitungDiskon(85000.0, 150.0) dipanggil dari baris 15. Perhatikan bahwa nilai kedua argument tercetak apa adanya; dari sinilah kita langsung melihat pelakunya: 150.0 padahal batas atasnya 100.
  3. Frame #1: pemanggilan di bawahnya — prosesKeranjang(Array) dari baris 21. Argument bertipe array hanya diringkas menjadi Array; isinya harus diperiksa di kode sumber.
  4. Frame #2 {main}: frame terakhir adalah skrip utama — titik masuk eksekusi tempat $keranjang didefinisikan dan prosesKeranjang() dipanggil.
  5. Baris thrown in ... on line 5: penutup yang mengulang lokasi throw, berguna saat layar digulir jauh dari baris utama.

Membaca Trace untuk Menemukan Penyebab, Bukan Sekadar Lokasi #

Trace menunjukkan tempat exception dilempar, tetapi tempat tersebut belum tentu lokasi kesalahan sebenarnya. Pada contoh di atas, hitungDiskon() sudah bekerja sesuai kontraknya: menolak persentase di luar rentang 0–100. Akar masalahnya justru berada di frame #2 {main} pada baris 18, tempat $keranjang didefinisikan dengan "diskon" => 150.0 — nilai yang seharusnya 15.0.

Karena itu, setelah membaca frame #0, telusuri frame di bawahnya satu per satu sambil membuka berkas dan baris yang dirujuk. Tanyakan pada tiap frame: nilai apa yang dikirim ke bawah, dan apakah nilainya masuk akal? Pola pikir ini membedakan pembaca trace pemula — yang berhenti di pesan error — dengan pembaca yang menemukan akar masalah.

Mendapatkan Trace dari Exception yang Ditangkap #

Saat exception ditangkap oleh blok catch, PHP tidak mencetak apa pun secara otomatis. Method getTraceAsString() pada object exception mengembalikan trace dalam format yang sama dengan output fatal, sehingga kita bisa mencatatnya atau menampilkannya sendiri. Tiga method pendamping — getMessage(), getFile(), dan getLine() — melengkapi informasi pesan dan lokasi:

php
<?php
function ambilKunci(array $konfigurasi, string $nama): string
{
    if (!array_key_exists($nama, $konfigurasi)) {
        throw new RuntimeException("Kunci konfigurasi '$nama' tidak ditemukan");
    }

    return $konfigurasi[$nama];
}

$konfigurasi = ["app_name" => "Toko Budi"];

try {
    echo ambilKunci($konfigurasi, "database_host") . "\n";
} catch (RuntimeException $e) {
    echo "Pesan  : " . $e->getMessage() . "\n";
    echo "Berkas : " . $e->getFile() . "\n";
    echo "Baris  : " . $e->getLine() . "\n";
    echo "Trace  :\n" . $e->getTraceAsString() . "\n";
}

Output yang Dihasilkan:

text
Pesan  : Kunci konfigurasi 'database_host' tidak ditemukan
Berkas : /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php
Baris  : 5
Trace  :
#0 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(14): ambilKunci(Array, 'database_host')
#1 {main}

Pada blok catch, method getFile() mengembalikan path berkas tempat throw terjadi dan getLine() mengembalikan nomor barisnya. Trace dari getTraceAsString() memuat dua frame: #0 untuk pemanggilan ambilKunci(Array, 'database_host') dari baris 14, dan #1 {main} untuk skrip utama. Format inilah yang nantinya kita salin ke berkas log ketika membahas logging terstruktur pada chapter-chapter berikutnya.

Menelusuri Trace yang Melintasi Beberapa Berkas #

Aplikasi nyata tidak hidup dalam satu berkas, dan trace akan melompat antar berkas mengikuti alur require. Bundle pendamping chapter ini menyediakan berkas pelacak.php yang mendefinisikan dua function lapisan aplikasi. Skrip berikut me-require berkas tersebut lalu memicu kegagalan di dalamnya:

php
<?php
require "pelacak.php";

$pesanan = ["id" => 1042, "pelanggan" => "Budi"];
prosesPesanan($pesanan);

Output yang Dihasilkan:

text

Fatal error: Uncaught RuntimeException: Koneksi database gagal: server tidak merespons in /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/pelacak.php:7
Stack trace:
#0 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/pelacak.php(12): simpanKeDatabase(Array)
#1 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(5): prosesPesanan(Array)
#2 {main}
  thrown in /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/pelacak.php on line 7

Perhatikan bahwa trace kini memuat dua berkas berbeda. Frame #0 menunjuk pelacak.php baris 12 — pemanggilan simpanKeDatabase(Array) dari dalam prosesPesanan() — dan frame #1 menunjuk berkas skrip utama baris 5. Baris utama fatal sendiri menunjuk pelacak.php:7, tempat throw berada. Untuk menelusuri kasus seperti ini, buka setiap berkas yang disebut frame secara berurutan mulai dari #0; jangan mengira seluruh cerita ada di berkas pertama.

Mencetak Trace Aktif dengan debug_print_backtrace #

Kadang kita ingin melihat jalur panggilan saat program masih sehat, misalnya untuk memahami siapa yang memanggil sebuah function. Function debug_print_backtrace() mencetak trace saat itu juga tanpa perlu exception sama sekali:

php
<?php
function lapisanC(): void
{
    debug_print_backtrace();
}

function lapisanB(): void
{
    lapisanC();
}

function lapisanA(): void
{
    lapisanB();
}

lapisanA();

Output yang Dihasilkan:

text
#0 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(9): lapisanC()
#1 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(14): lapisanB()
#2 /home/z/my-project/php-course/content/php/debugging-dan-observability/membaca-stack-traces/.verify_tmp.php(17): lapisanA()

Outputnya memakai format ringkas #urutan fungsi() called at [berkas:baris] dengan frame terbaru di atas, sama seperti trace exception. Satu perbedaan kecil: debug_print_backtrace() tidak mencetak frame {main} penutup, jadi daftar berhenti di frame lapisanA(). Kalimat “called at” menunjukkan lokasi pemanggil — bukan lokasi definisi function — sehingga kita tetap bisa melompat langsung ke baris yang relevan.

Membaca berkas:baris di terminal menjadi jauh lebih cepat bila editor kita bisa membukanya langsung. Mayoritas IDE modern — PhpStorm, VS Code, Zend-debugger-aware terminal — mengenali pola path/berkas.php(baris) atau berkas.php pada baris N pada output error dan menjadikannya tautan yang bisa diklik atau Ctrl+klik. Satu klik akan membuka berkas tepat di baris yang dimaksud frame.

Bila terminal Anda tidak mengenali polanya, biasakan menyalin path dan nomor baris secara manual lalu membukanya dengan fitur “go to line” editor (Ctrl+G di banyak editor). Kombinasi kebiasaan ini — baca frame #0 dulu, buka berkasnya, turun ke frame berikutnya bila perlu — adalah alur kerja debugging harian yang akan dipakai terus-menerus, termasuk saat kita beralih ke debugger interaktif Xdebug pada chapter berikutnya.

Ringkasan #

Stack trace merekam rantai panggilan dari skrip utama hingga titik kegagalan, dengan frame terbaru (#0) di paling atas dan frame {main} sebagai pemanggil paling luar. Setiap frame memuat berkas, baris pemanggilan, nama function, dan nilai argument skalar. Trace menunjukkan lokasi kegagalan; akar masalah kerap berada di frame pemanggil yang mengirim data buruk, sehingga menelusuri beberapa frame lintas berkas adalah kebiasaan wajib. Dengan getTraceAsString() dan debug_print_backtrace(), jejak eksekusi juga bisa didapat tanpa menghentikan program. Setelah mampu membaca jejak statis ini, kita siap mengendalikan eksekusi secara langsung melalui Xdebug.

Referensi #

Berkas Sumber

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

pelacak.php

Unduh berkas
php
<?php
// Bundle pendamping chapter Membaca Stack Traces.
// Berkas ini menyimulasikan lapisan aplikasi yang hidup di berkas terpisah.

function simpanKeDatabase(array $pesanan): void
{
    throw new RuntimeException("Koneksi database gagal: server tidak merespons");
}

function prosesPesanan(array $pesanan): void
{
    simpanKeDatabase($pesanan);
    echo "Pesanan {$pesanan["id"]} tersimpan\n";
}