โ† Semua pembelajaran / Laravel Nol โ†’ Enterprise
Fase 1 ยท Request, Routing & Blade

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.

Sumber asli laravel.com Resmi Rangkuman ~6 menit baca

Intisari

  • Semua exception yang tidak tertangkap masuk ke ->withExceptions(...) di bootstrap/app.php.
  • APP_DEBUG=true menampilkan stack trace berikut isi environment โ€” di produksi ini membocorkan kredensial.
  • Exception domain sendiri yang punya render() jauh lebih rapi daripada try/catch di 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

PemicuStatusMuncul saat
abort(404) / model binding gagal404Data tidak ada
abort(403) / policy menolak403Ada, tapi bukan haknya
Validasi gagal422Bentuk data salah
Belum login401 / redirect ke loginTergantung expectsJson()
Rate limit terlampaui429Fase 4
Exception tak tertangani500Bug โ€” 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.