Program sejati selalu bertemu kondisi tak terduga: data yang tidak lengkap, berkas yang hilang, koneksi database yang putus. PHP menangani situasi tersebut lewat mekanisme exception: object khusus yang dilempar (throw) untuk memutus alur normal, dan bisa ditangkap (catch) untuk dipulihkan. Laravel membangun satu lapisan lagi di atasnya — global exception handler yang mengubah exception menjadi response HTTP yang rapi. Chapter ini menyegarkan konsep PHP-nya terlebih dahulu, lalu melihat bagaimana Laravel menerjemahkan tiap exception menjadi halaman 404, redirect validasi, atau halaman error.

throw dan try/catch #

Exception dibuat dan dilempar dengan throw new ...Exception(...). Alur program berhenti seketika sampai ada blok catch yang menangkapnya. Mari lihat mekanismenya dalam PHP murni:

php
<?php
function bagi(float $a, float $b): float
{
    if ($b === 0.0) {
        throw new InvalidArgumentException('Pembagi tidak boleh nol');
    }

    return $a / $b;
}

try {
    echo bagi(10, 2) . "\n";
    echo bagi(10, 0) . "\n";
} catch (InvalidArgumentException $e) {
    echo 'Tertangkap: ' . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
5
Tertangkap: Pembagi tidak boleh nol

Pemanggilan pertama berjalan normal. Pada pemanggilan kedua, bagi() melempar InvalidArgumentException, baris echo bagi(10, 0) tidak pernah menghasilkan output, dan eksekusi melompat langsung ke blok catch. Method getMessage() milik class Exception mengembalikan teks yang kita kirim ke constructor. Perhatikan bahwa echo bagi(10, 2) di dalam try tetap tereksekusi — hanya baris setelah pelemparan yang dilewati.

Custom Exception Class dan finally #

Pada aplikasi nyata kita biasanya membuat class exception sendiri agar tiap jenis masalah bisa ditangkap secara terpisah. Class tersebut cukup extends salah satu class exception bawaan PHP. Blok finally melengkapi struktur: kode di dalamnya dijalankan apa pun hasilnya — berhasil, tertangkap, maupun exception yang lolos:

php
<?php
class StokHabisException extends RuntimeException
{
    public function __construct(public readonly string $produk)
    {
        parent::__construct("Stok produk {$produk} habis");
    }
}

function pesan(string $produk, int $stok): string
{
    if ($stok < 1) {
        throw new StokHabisException($produk);
    }

    return "Pesanan {$produk} diterima";
}

try {
    echo pesan('Kopi Gayo', 5) . "\n";
    echo pesan('Kopi Toraja', 0) . "\n";
} catch (StokHabisException $e) {
    echo 'Tertangkap: ' . $e->getMessage() . "\n";
    echo 'Produk: ' . $e->produk . "\n";
} finally {
    echo "Blok finally selalu dijalankan\n";
}

Output yang Dihasilkan:

text
Pesanan Kopi Gayo diterima
Tertangkap: Stok produk Kopi Toraja habis
Produk: Kopi Toraja

Constructor StokHabisException memakai constructor property promotion sekaligus memanggil parent::__construct() agar pesan error tetap tersimpan. Berkat itu, object exception membawa data tambahan ($e->produk) yang bisa dibaca penangkapnya — teknik yang banyak dipakai exception kelas Laravel untuk membawa konteks.

Hirarki Throwable: Error vs Exception #

Seluruh yang bisa dilempar di PHP mengimplementasikan interface Throwable, yang bercabang menjadi dua keluarga besar. Exception untuk masalah yang wajar ditangkap aplikasi, dan Error untuk kegagalan engine seperti memanggil method yang tidak ada:

php
<?php
try {
    $hasil = 10 % 0;
} catch (DivisionByZeroError $e) {
    echo 'Error: ' . get_class($e) . ' - ' . $e->getMessage() . "\n";
}

$tugas = new DateTime();

try {
    $tugas->metodeTidakAda();
} catch (Error $e) {
    echo 'Error: ' . get_class($e) . ' - ' . $e->getMessage() . "\n";
}

var_dump(is_subclass_of('DivisionByZeroError', 'Error'));
var_dump(is_subclass_of('RuntimeException', 'Exception'));
var_dump(is_subclass_of('Exception', 'Throwable'));
var_dump(is_subclass_of('Error', 'Throwable'));

Output yang Dihasilkan:

text
Error: DivisionByZeroError - Modulo by zero
Error: Error - Call to undefined method DateTime::metodeTidakAda()
bool(true)
bool(true)
bool(true)
bool(true)

Hubungan keempat nama tersebut dirangkum di sini:

ClassIndukContohKapan muncul
Throwable(interface)—Kontrak semua yang bisa dilempar
ExceptionThrowableRuntimeException, InvalidArgumentExceptionKesalahan logika aplikasi
ErrorThrowableDivisionByZeroError, TypeErrorKegagalan level engine
Custom exceptionExceptionStokHabisExceptionDomain spesifik aplikasi kita

Exception di Tangan Laravel #

Di aplikasi Laravel kita jarang menulis try/catch untuk hal umum, karena framework mendaftarkan satu penangkap global di lapisan paling luar: setiap request yang melempar exception akan ditangkap, diterjemahkan menjadi response, dan dicatat ke log. Pusat konfigurasinya ada di bootstrap/app.php. Berikut isi asli berkas tersebut dari sandbox Course ini:

php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        //
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->shouldRenderJsonWhen(
            fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
        );
    })->create();

Method withExceptions() menerima closure untuk mengatur perilaku handler global — pola higher-order function dari chapter sebelumnya. Terjemahan exception ke response HTTP juga terjadi otomatis untuk exception bawaan. Ketika User::findOrFail($id) tidak menemukan data, Eloquent melempar ModelNotFoundException yang oleh handler diubah menjadi response 404 Not Found; helper abort(403, ...) melempar HttpException milik Symfony yang diterjemahkan menjadi response 403 Forbidden. Kita bisa membuktikan keduanya lewat feature test pada sandbox:

php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExceptionHandlingTest extends TestCase
{
    use RefreshDatabase;

    public function test_model_tidak_ditemukan_menjadi_404(): void
    {
        $response = $this->get('/pengguna/999');

        $response->assertStatus(404);
    }

    public function test_abort_403_menampilkan_status_dilarang(): void
    {
        $response = $this->get('/hanya-admin');

        $response->assertStatus(403);
        $response->assertSee('Akses khusus admin.');
    }
}

Output yang Dihasilkan:

text
  PASS  Tests\Feature\ExceptionHandlingTest
  ✓ model tidak ditemukan menjadi 404                                   0.16s
  ✓ abort 403 menampilkan status dilarang                                0.01s

  Tests:    2 passed (3 assertions)
  Duration: 0.25s

Dua route di balik pengujian tersebut juga hanya berisi closure: route /pengguna/{id} memanggil User::findOrFail($id) di dalam tubuh closure-nya, sedangkan route /hanya-admin memanggil abort(403, 'Akses khusus admin.') — keduanya tidak punya try/catch sama sekali, karena handler global yang bekerja. Satu kasus terakhir yang sering kita temui: ValidationException yang otomatis melempar response 422 (untuk request JSON) atau redirect kembali ke form. Runner berikut menunjukkan isi exception tersebut saat validasi gagal:

php
<?php

use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\Validator;

try {
    Validator::make(
        ['judul' => 'Baru'],
        ['judul' => 'required|min:5'],
    )->validate();
} catch (ValidationException $e) {
    echo 'Kode status: ' . $e->status . "\n";
    echo json_encode($e->errors(), JSON_PRETTY_PRINT) . "\n";
}

Output yang Dihasilkan:

text
Kode status: 422
{
    "judul": [
        "The judul field must be at least 5 characters."
    ]
}

Property $e->status dan $e->errors() adalah data tambahan yang dibawa class exception — persis pola custom exception yang kita tulis di bagian awal chapter. Materi dasar exception PHP bisa dibaca di dokumentasi resmi error handling PHP , sedangkan daftar lengkap cara mengustomisasi handler Laravel ada di dokumentasi error handling Laravel.

Dengan exception terkelola, aplikasi kita bisa gagal dengan sopan. Topik penutup section ini membahas tangan yang menyusun semua komponen tersebut: Composer — pengelola dependency yang mengunduh framework, paket pendukung, sekaligus mengatur autoload yang kita bahas di chapter sebelumnya.