0

The Laravel exception handler — App\Exceptions\Handler deep dive

Advanced5 min read·php-10-009
laravel-srcinterview

Concept

Laravel's App\Exceptions\Handler is the central exception processing hub. Every exception that isn't caught in a try/catch bubbles up to this class, which decides whether to log it and how to render it as an HTTP response.

Two key methods:

  • report(\Throwable $e): Called for every uncaught exception. Responsible for logging/alerting. Calls parent::report() which invokes registered logger integrations (Sentry, Bugsnag, Flare via the \Illuminate\Log\Logger).
  • render($request, \Throwable $e): Called to convert the exception to an HTTP response. Returns a \Symfony\Component\HttpFoundation\Response.

$dontReport: Array of exception classes that should NOT be logged (e.g., ValidationException, AuthenticationException). These are expected errors that don't indicate bugs.

$dontFlash: Array of form input keys that should not be flashed to the session (e.g., password, password_confirmation) for security.

reportable() / renderable(): Laravel 8+ added fluent registration of per-exception callbacks in register():

php
$this->reportable(fn(PaymentException $e) => Slack::alert($e->getMessage()));
$this->renderable(fn(UserNotFoundException $e, $request) => response()->json(['error' => 'User not found'], 404));

$withoutExceptionHandling in tests: $this->withoutExceptionHandling() in a test throws exceptions directly instead of routing through the handler — useful when you want to see raw exceptions in test output rather than HTTP responses.

Code Example

php
<?php
// app/Exceptions/Handler.php
namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request;
use Throwable;

class Handler extends ExceptionHandler
{
    /**
     * Exceptions NOT logged (expected business errors)
     */
    protected $dontReport = [
        \App\Exceptions\UserNotFoundException::class,
        \App\Exceptions\OrderAlreadyShippedException::class,
    ];

    /**
     * Input fields NOT flashed to session on validation error
     */
    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    public function register(): void
    {
        // Per-exception reporting
        $this->reportable(function (\App\Exceptions\PaymentException $e): void {
            // Send to Slack or PagerDuty for payment failures
            SlackNotifier::critical("Payment failure: " . $e->getMessage());
        });

        // Per-exception rendering
        $this->renderable(function (\App\Exceptions\UserNotFoundException $e, Request $request) {
            if ($request->expectsJson()) {
                return response()->json(['message' => $e->getMessage()], 404);
            }
            return response()->view('errors.user-not-found', [], 404);
        });

        // Global fallback for API requests
        $this->renderable(function (\Throwable $e, Request $request) {
            if ($request->expectsJson()) {
                $statusCode = method_exists($e, 'getStatusCode')
                    ? $e->getStatusCode()
                    : 500;
                return response()->json([
                    'message' => app()->environment('production')
                        ? 'Server Error'
                        : $e->getMessage(),
                ], $statusCode);
            }
        });
    }
}