Aplikasi yang sehat tetap bisa melempar exception: model tidak ditemukan, input tidak valid, session kadaluarsa. Jika setiap route harus membungkus logikanya dengan try/catch sendiri, kode kita akan dipenuhi perekaman yang sama berulang-ulang. Laravel menyelesaikan masalah ini dengan satu penangkap global di bootstrap/app.php: semua exception yang lolos dari kode kita ditangkap framework, dicatat bila perlu, lalu diubah menjadi response yang sesuai — 404 untuk data hilang, 422 untuk validasi gagal, redirect ke halaman login untuk tamu. Pada chapter ini kita bedah alur tersebut dari exception sampai response, dibuktikan dengan test sungguhan.

Handler Global di bootstrap/app.php #

Sejak Laravel 11, aplikasi tidak lagi memiliki class App\Exceptions\Handler terpisah. Konfigurasi exception hidup di bootstrap/app.php lewat method withExceptions(). Berikut isi file tersebut pada instalasi Laravel 13 yang masih segar:

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 objek Exceptions yang menjadi pintu kustomisasi: shouldRenderJsonWhen() menentukan kapan error dirender sebagai JSON, sedangkan reportable() dan renderable() akan kita pakai di chapter Exception Reporting . Satu file ini adalah “tempat parkir” seluruh strategi error aplikasi kita.

Alur Render: dari Exception ke Response #

Saat sebuah route melempar exception, pipeline di bawah layar bekerja berurutan sebelum response akhir dikirim ke browser:

  1. mapException() — exception diubah lewat pemetaan kustom jika terdaftar.
  2. Method render() milik exception — jika class exception kita sendiri punya method render(), hasilnya langsung dipakai.
  3. prepareException() — pemetaan bawaan framework, misalnya ModelNotFoundException menjadi NotFoundHttpException (404).
  4. Callback renderable() — callback kustom dari withExceptions() mendapat kesempatan menghasilkan response.
  5. Pencocokan kelas — AuthenticationException diarahkan ke halaman login atau JSON 401; ValidationException me-redirect balik dengan errors atau JSON 422; sisanya dirender sebagai halaman error dengan kode statusnya.

Hasil akhirnya selalu sebuah Response — pengguna tidak pernah melihat mentahan stack trace kecuali APP_DEBUG aktif.

Pemetaan Bawaan Framework #

Laravel sudah mengenali banyak exception domain dan menerjemahkannya otomatis. Tabel berikut dirangkum dari kode sumber Illuminate\Foundation\Exceptions\Handler:

ExceptionMenjadiEfek ke pengguna
ModelNotFoundExceptionNotFoundHttpException404
AuthorizationExceptionHttpException (403) atau AccessDeniedHttpException403
TokenMismatchExceptionHttpException (419)419, halaman “Page Expired”
ValidationExceptionredirect balik atau JSON302 dengan errors (web), 422 (JSON)
AuthenticationExceptionredirect login atau JSON302 (web), 401 (JSON)
HttpException (via abort())dirinya sendirikode status dari abort()

Karena pemetaan ini sudah ada, rute findOrFail() yang gagal otomatis berperilaku benar tanpa satu baris try/catch pun.

Bukti Nyata: ModelNotFound menjadi 404 #

Kita uji klaim di atas dengan route yang melempar ModelNotFoundException sungguhan, lalu feature test yang memeriksa status response:

php
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Route;

Route::get('/post/{id}', function (string $id) {
    $exception = new ModelNotFoundException();
    $exception->setModel('App\Models\Post', [(int) $id]);

    throw $exception;
})->name('post.show');
php
public function test_model_not_found_menjadi_404(): void
{
    $response = $this->get('/post/7');

    fwrite(STDERR, 'status: ' . $response->status() . "\n");
    $body = trim(preg_replace('/\s+/', ' ', $response->getContent()));
    fwrite(STDERR, 'title: ' . (strstr($body, '<title>') ? 'Not Found' : '?') . "\n");

    $this->assertSame(404, $response->status());
}

Output yang Dihasilkan:

text
status: 404
title: Not Found

Skenario tetangga yang kita uji bersamaan memperlihatkan pemetaan lainnya: POST form dengan input "ab" gagal rule min:3 menghasilkan 302 balik ke halaman asal (sesi web) atau 422 dengan body JSON errors (request Accept: application/json), dan AuthenticationException me-redirect tamu ke http://localhost:8000/login atau membalas 401 dengan body {"message":"Unauthenticated."} untuk request JSON.

try/catch Lokal Saat Diperlukan #

Handler global mengurus presentasi error, bukan keputusan bisnis. Ketika sebuah kegagalan punya jalan keluar yang wajar — nilai cadangan, mode offline — penangkapan lokal lebih jujur daripada membiarkannya menjadi error 500:

php
Route::get('/kutipan-hari-ini', function () {
    try {
        return ambilKutipan();
    } catch (RuntimeException $e) {
        return 'Kutipan cadangan: "Kode yang baik adalah kode yang bisa dibaca."';
    }
});

if (! function_exists('ambilKutipan')) {
    function ambilKutipan(): string
    {
        throw new RuntimeException('Sumber kutipan sedang tidak tersedia.');
    }
}

Output yang Dihasilkan:

text
status: 200
body: Kutipan cadangan: "Kode yang baik adalah kode yang bisa dibaca."

Aturan praktisnya: gunakan try/catch hanya ketika kita punya rencana pemulihan yang jelas; biarkan sisanya menggelegak ke handler global agar tercatat di log dan mendapat response yang konsisten. Daftar lengkap opsi kustomisasi ada di dokumentasi Errors . Setelah response terkirim, pertanyaan berikutnya muncul: exception mana yang perlu dicatat? Chapter Exception Reporting menjawabnya dengan report() dan reportable().