Docs LATEST

Exception Handling Configuration

Configure custom error rendering and reporting.

Configuration

Exception handling configuration lives in config/exceptions.php. Customize how exceptions are displayed to users and how they're logged.

Full Configuration Example #

return [
    // Custom renderers: exception class => renderer callable
    'renderers' => [
        \App\Exceptions\MaintenanceException::class => function (Throwable $e, Request $request) {
            return Response::html(
                view('errors/maintenance', ['message' => $e->getMessage()]),
                503
            );
        },
        
        \VelvetCMS\Exceptions\ValidationException::class => function (Throwable $e, Request $request) {
            if ($request->wantsJson()) {
                return Response::json(['errors' => $e->errors()], 422);
            }
            
            return Response::redirect()->back()->withErrors($e->errors());
        },
    ],
    
    // Custom reporters: exception class => reporter callable
    'reporters' => [
        \PDOException::class => function (Throwable $e, Request $request, $logger) {
            $logger->critical('Database error', [
                'message' => $e->getMessage(),
                'query' => $e->queryString ?? null,
                'url' => $request->uri(),
            ]);
        },
        
        \RuntimeException::class => function (Throwable $e, Request $request, $logger) {
            // Send to external error tracking
            Sentry::captureException($e);
        },
    ],
];

Renderers #

Renderers convert exceptions into HTTP responses. The exception handler checks for a matching renderer and uses it instead of the default error page.

Signature:

function(Throwable $e, Request $request): Response

Matching Rules #

  • Exact class matches are checked first
  • Parent classes are not matched (be specific)
  • First matching renderer wins
  • If no renderer matches, the default handler runs

Common Patterns #

Custom 404 page:

\VelvetCMS\Exceptions\NotFoundException::class => function ($e, $request) {
    return Response::html(view('errors/404'), 404);
},

API error responses:

\App\Exceptions\ApiException::class => function ($e, $request) {
    return Response::json([
        'error' => $e->getMessage(),
        'code' => $e->getCode(),
    ], $e->getStatusCode());
},

Maintenance mode:

\App\Exceptions\MaintenanceException::class => function ($e, $request) {
    return Response::html(view('errors/maintenance'), 503)
        ->header('Retry-After', 3600);
},

Reporters #

Reporters handle logging and external error tracking. They run before rendering, so you can log errors even if rendering fails.

Signature:

function(Throwable $e, Request $request, LoggerInterface $logger): void

Common Patterns #

External error tracking:

\Throwable::class => function ($e, $request, $logger) {
    Bugsnag::notifyException($e);
},

Slack notifications for critical errors:

\App\Exceptions\CriticalException::class => function ($e, $request, $logger) {
    $logger->critical($e->getMessage());
    SlackNotifier::alert("Critical error: {$e->getMessage()}");
},

Skip logging for specific exceptions:

\App\Exceptions\ExpectedValidationError::class => function ($e, $request, $logger) {
    // Don't log validation errors-they're expected
},

Default Behavior #

Without custom configuration:

  • Production (debug = false): Shows a generic error page
  • Development (debug = true): Shows detailed stack trace
  • Logging: All exceptions logged to storage/logs/

Tips #

  • Keep renderers fast-they run on every error
  • Use reporters for external services, not renderers
  • Test your custom handlers with each exception type
  • Consider user experience: helpful messages without exposing internals