Error & exception handling
Penanganan error yang baik menjawab dua pertanyaan berbeda sekaligus: apa yang perlu diketahui pengguna, dan apa yang perlu diketahui kamu. Keduanya jarang sama.
Intisari
- Semua exception yang tidak tertangkap masuk ke
->withExceptions(...)dibootstrap/app.php. APP_DEBUG=truemenampilkan stack trace berikut isi environment โ di produksi ini membocorkan kredensial.- Exception domain sendiri yang punya
render()jauh lebih rapi daripadatry/catchdi setiap controller. report()mencatat tanpa menghentikan;abort()menghentikan dengan status HTTP.- Untuk API, pastikan respons error tetap JSON โ bukan halaman HTML yang membingungkan klien.
Konfigurasi terpusat
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
// Jangan penuhi log dengan hal yang memang wajar terjadi
$exceptions->dontReport([PembayaranDitolak::class]);
// Konteks tambahan untuk SETIAP laporan error
$exceptions->context(fn () => [
'tenant_id' => auth()->user()?->tenant_id,
]);
// Bentuk respons untuk exception tertentu
$exceptions->render(function (KuotaHabis $e, Request $request) {
return $request->expectsJson()
? response()->json(['message' => $e->getMessage()], 429)
: back()->with('galat', $e->getMessage());
});
})
Exception domain: satu kelas, satu makna
class StokTidakCukup extends Exception
{
public function __construct(
public readonly Produk $produk,
public readonly int $diminta,
) {
parent::__construct("Stok {$produk->nama} tinggal {$produk->stok}, diminta {$diminta}.");
}
// Laravel memanggil ini otomatis saat exception lolos ke atas
public function render(Request $request): Response
{
return $request->expectsJson()
? response()->json([
'message' => $this->getMessage(),
'produk_id' => $this->produk->id,
'tersedia' => $this->produk->stok,
], 409)
: back()->withErrors(['jumlah' => $this->getMessage()]);
}
// Konteks yang ikut ke log
public function context(): array
{
return ['produk_id' => $this->produk->id, 'diminta' => $this->diminta];
}
}
Sekarang layanan domainmu cukup throw new StokTidakCukup($produk, 5). Tidak ada satu pun controller
yang perlu tahu cara menampilkannya, dan bentuk responsnya konsisten di seluruh aplikasi.
Kenapa ini lebih baik daripada try/catch di controller. Dengan try/catch,
penanganan yang sama ditulis ulang di setiap tempat pemanggilan, dan satu tempat yang terlewat menghasilkan
halaman 500. Dengan exception yang tahu cara merender dirinya, penanganan ditulis sekali dan berlaku di semua
tempat โ termasuk di job antrean dan perintah Artisan.
Status HTTP yang sudah disediakan
| Pemicu | Status | Muncul saat |
|---|---|---|
abort(404) / model binding gagal | 404 | Data tidak ada |
abort(403) / policy menolak | 403 | Ada, tapi bukan haknya |
| Validasi gagal | 422 | Bentuk data salah |
| Belum login | 401 / redirect ke login | Tergantung expectsJson() |
| Rate limit terlampaui | 429 | Fase 4 |
| Exception tak tertangani | 500 | Bug โ harus memicu alarm |
404 versus 403 adalah keputusan keamanan. Menjawab 403 untuk dokumen milik orang lain memberi tahu penyerang bahwa dokumen itu ada. Untuk data sensitif, jawab 404 saja โ Laravel melakukannya secara default saat kamu memfilter query berdasarkan pemilik, karena barisnya memang tidak ditemukan.
APP_DEBUG di produksi
Dengan APP_DEBUG=true, halaman error Laravel menampilkan stack trace, potongan kode sumber,
dan tabel berisi seluruh variabel environment โ termasuk password database, kunci API, dan
APP_KEY. Satu error 500 yang terlihat publik sudah cukup untuk membocorkan seluruh aplikasi.
# Wajib di produksi
APP_DEBUG=false
APP_ENV=production
Log yang berguna dibaca mesin
Log::info('pesanan dibuat', [
'pesanan_id' => $pesanan->id,
'total' => $pesanan->total,
'user_id' => $pesanan->user_id,
]);
// Melaporkan tanpa menggagalkan permintaan
try {
$pengirim->kirim($pesan);
} catch (Throwable $e) {
report($e); // masuk log & error tracker
// permintaan tetap lanjut
}
Di container, log harus ke stderr. Setel LOG_CHANNEL=stderr supaya keluaran
ditangkap runtime kontainer dan diteruskan ke CloudWatch (Fase 9). Menulis ke
storage/logs/laravel.log di dalam kontainer berarti log hilang saat task diganti โ dan tidak ada
yang bisa membacanya saat insiden.
Latihan: buat exception StokTidakCukup beserta render()-nya, lempar dari
sebuah rute, lalu panggil rute itu dua kali: sekali dengan curl biasa, sekali dengan
-H "Accept: application/json". Pastikan yang satu HTML dan yang satu JSON 409. Lalu set
APP_DEBUG=false dan lihat betapa berbedanya halaman yang muncul.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.