Bug paling menakutkan bukan yang meledak dengan pesan jelas, melainkan yang bekerja diam-diam lalu merusak data tiga hari kemudian. Menelusurinya butuh alur kerja, bukan keberuntungan: mulai dari halaman error yang informatif, baca stack trace dengan benar, gunakan log sebagai titik awal, dan bantu diri sendiri dengan alat cetak sementara. Pada chapter ini kita susun alur kerja tersebut di sekitar tiga senjata utama — APP_DEBUG, laravel.log, dan dd() — semuanya dibuktikan dengan output asli dari route yang sengaja kita crash-kan.

APP_DEBUG dan Halaman Error #

Ketika APP_DEBUG=true, exception yang tidak tertangkap menampilkan halaman error penuh, bukan “Server Error” kosong. Route uji kita:

php
Route::get('/crash', function () {
    throw new RuntimeException('Gagal menyambung ke layanan pengiriman.');
});

Halaman yang dikirim Laravel 13 untuk request tersebut berukuran 881.467 byte — kontennya adalah halaman error interaktif lengkap. Bagian terpentingnya, versi teks dari halaman tersebut, tampak seperti ini:

Output yang Dihasilkan:

text
RuntimeException - Internal Server Error

Gagal menyambung ke layanan pengiriman.

PHP 8.4.8
Laravel 13.36.0
localhost:8000

## Stack Trace

0 - routes/web.php:62
1 - vendor/laravel/framework/src/Illuminate/Routing/CallableDispatcher.php:39
2 - vendor/laravel/framework/src/Illuminate/Routing/Route.php:254
3 - vendor/laravel/framework/src/Illuminate/Routing/Route.php:219
4 - vendor/laravel/framework/src/Illuminate/Routing/Router.php:834
5 - vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:180
6 - app/Http/Middleware/TandaiRequest.php:19
7 - vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php:219
...

Header halaman langsung memberi tiga fakta kunci: class exception (RuntimeException), pesannya, dan lingkungan (PHP 8.4.8, Laravel 13.36.0). Halaman ini juga memuat potongan kode di sekitar baris yang salah, daftar request data, dan nilai environment — semuanya di browser, tanpa membuka terminal.

Membaca Stack Trace #

Urutan frame stack trace dibaca dari atas ke bawah, dan dua frame teratas hampir selalu cukup: frame 0 - routes/web.php:62 adalah titik lempar sesungguhnya di kode kita. Frame di bawahnya hanyalah “panggung” framework yang meneruskan eksekusi. Kuncinya membedakan dua jenis frame:

  • Frame app/ dan routes/ — kode milik kita; frame app/Http/Middleware/TandaiRequest.php:19 menunjukkan middleware kita ikut dalam rantai panggilan.
  • Frame vendor/ — kode framework; jarang jadi sumber bug, tapi berguna untuk memahami jalur request.

Log file punya versi yang lebih teknis dan lebih awet — trace yang sama tertulis saat exception dilaporkan, dengan format file(line): call:

text
[2026-10-13 19:56:39] testing.ERROR: Gagal menyambung ke layanan pengiriman. {"exception":"[object] (RuntimeException(code: 0): Gagal menyambung ke layanan pengiriman. at .../routes/web.php:62)
[stacktrace]
#0 .../vendor/laravel/framework/src/Illuminate/Routing/CallableDispatcher.php(39): Illuminate\\Routing\\RouteFileRegistrar->{closure:.../routes/web.php:61}()
#1 .../vendor/laravel/framework/src/Illuminate/Routing/Route.php(254): Illuminate\\Routing\\CallableDispatcher->dispatch(Object(Illuminate\\Routing\\Route), Object(Closure))
#2 .../vendor/laravel/framework/src/Illuminate/Routing/Route.php(219): Illuminate\\Routing\\Route->runCallable()
#3 .../vendor/laravel/framework/src/Illuminate/Routing/Router.php(834): Illuminate\\Routing\\Route->run()
"}

Frame #0 kembali menunjuk routes/web.php:61 — closure route yang melempar. Perhatikan pula konteks {"request_id":"req-tanpa-id","user_id":null} di baris yang sama: middleware dari chapter Contextual Logging ikut menandai log ini, sehingga satu grep request_id bisa menarik seluruh jejak request yang bermasalah.

Log sebagai Titik Awal #

Sebelum membuka editor, tanyakan dulu: “apakah log sudah bicara?” Sesi debugging yang sehat dimulai dari ekor file:

bash
tail -n 50 storage/logs/laravel.log

Jika error sudah terjadi sebelumnya, baris ERROR dengan konteksnya menghemat puluhan menit. Untuk menambah jejak di titik yang dicurigai, naikkan sementara level pencatatan menjadi Log::debug('sampai di sini', ['order' => $order->id]) di beberapa titik kode — jejak yang berhenti di baris tertentu langsung mempersempit area pencarian. Usai sesi, hapus kembali baris debug itu agar log tidak berubah menjadi kertas koran.

dd() dan dump(): Cetak Lalu Berhenti #

Untuk memeriksa nilai variabel di titik tertentu, dd() (dump and die) mencetak isi variabel lalu menghentikan eksekusi, sedangkan dump() mencetak lalu melanjutkan:

php
dump(['produk' => 'Buku "Belajar Laravel"', 'harga' => 95000]);
echo "dump() tidak menghentikan eksekusi.\n";
dd(['produk' => 'Buku "Belajar Laravel"', 'harga' => 95000]);
echo "Baris ini tidak pernah tereksekusi.\n";

Output yang Dihasilkan:

text
array:2 [
  "produk" => "Buku "Belajar Laravel""
  "harga" => 95000
] // /tmp/tdd.php:7
dump() tidak menghentikan eksekusi.
array:2 [
  "produk" => "Buku "Belajar Laravel""
  "harga" => 95000
] // /tmp/tdd.php:9

Output dump menyertakan lokasi pemanggilan (// /tmp/tdd.php:7) sehingga kita tahu dari mana cetakan berasal. Baris terakhir contoh tidak pernah tereksekusi — dd() mematikan proses seketika.

Alat Bantu di Luar Core #

Tiga penyebut lain layak dikenal: Laravel Telescope menampilkan request, query, dan job langsung di UI untuk aplikasi lokal; Laravel Pulse memantau kesehatan runtime (queue, lambatnya query) — keduanya nonaktif saat testing lewat TELESCOPE_ENABLED=false/PULSE_ENABLED=false di phpunit.xml. Xdebug membawa step-debugger sungguhan: breakpoint, inspeksi variabel, eksekusi baris demi baris — jauh lebih presisi daripada dd() berlapis. Terakhir, test itu sendiri adalah alat debugging terbaik jangka panjang: bug yang sudah diperbaiki dan dibungkus regression test tidak akan kembali diam-diam.

Halaman error yang detail adalah kemewahan lokal — di produksi, halaman semacam itu justru bahaya. Chapter Environment-Specific Errors membahas apa yang berubah ketika APP_DEBUG dimatikan. Rujukan resmi untuk topik halaman error tersedia di dokumentasi Errors .