$ lexprog.com

// notes from an old coder -- php, databases, and the occasional rant

[May 06, 2025] Laravel

Laravel Error Handling: Custom Exceptions

Laravel Error Handling: Custom Exceptions

────────────────────────────────────────────────────────

Laravel Error Handling: Custom Exceptions

Tip: Custom Exception with Render

class PaymentFailed extends Exception
{
    public function render(Request $request): Response
    {
        return response()->json(['error' => 'Payment failed'], 402);
    }
}

Gotcha: Exception Handler Priority

$this->reportable(function (PaymentFailed $e) {
    // Custom reporting
});

Register reportable and renderable callbacks in App\Exceptions\Handler.

Tip: Report vs Render

report() logs the exception. render() returns an HTTP response. They're separate concerns.

Gotcha: Don't Report Expected Errors

protected $dontReport = [
    PaymentFailed::class,
];

Tip: Exception Context

public function report(): void
{
    Log::error('Payment failed', [
        'order_id' => $this->order->id,
        'amount' => $this->order->total,
    ]);
}

Gotcha: API vs Web Error Responses

Check expectsJson() to return different formats.

Tip: Use route:cache Carefully

php artisan route:cache is fast, but it doesn't work with closure-based routes. Every time you cache routes, Laravel serializes them. If you have Route::redirect() or closure callbacks, the cache breaks. Stick to controller-based routes in production.

Tip: Model APP_KEY Rotation

Rotating APP_KEY invalidates all encrypted data — cookies, encrypted DB columns, and password reset tokens. If you must rotate (e.g., after a leak), plan a migration that re-encrypts existing data with the new key.

Gotcha: Local Scope Leaks

Global scopes defined in booted() apply to ALL queries on that model — including relationships. An innocent User::all() in admin panel might exclude soft-deleted users if a global scope is active.

Senior Insight

Custom exception handling is where Laravel apps reveal their maturity level. The default exception handler is fine for prototypes, but production apps need granular rendering per exception type. I organize exceptions into categories — client errors (4xx), server errors (5xx), and third-party failures (timeouts, rate limits) — with distinct rendering and logging for each. And always report exceptions you don't handle; silent failures are the hardest to debug.

Source: Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/laravel), Spatie Blog (https://spatie.be/blog)

────────────────────────────────────────────────────────
<-- back to posts