
How to Customize Default Exceptions in Laravel 12: A Complete Guide
This article walks you through Laravel 12’s new withExceptions API, showing you how to centralize and customize every aspect of error handling—from routing and logging exceptions to different channels, setting custom log levels, and suppressing unwanted errors; to defining render callbacks that return tailored JSON or Blade views; to embedding report() and render() logic directly in your own exception classes; and finally publishing and styling your error pages. It’s a compact blueprint for making your app’s exception workflow both powerful and perfectly aligned with your project’s needs.
As we know Laravel 11 & 12 has major changes. In previous version of Laravel we was use Exception/Handler.php file. but in Laravel 11 & 12 bootstrap/app.php file handle this.
Laravel 12 ships with a powerful, configurable exception system. Instead of hacking the default Handler, you now use the withExceptions method in bootstrap/app.php to register all of your exception‐related callbacks against an Illuminate\Foundation\Configuration\Exceptions instance:
1// bootstrap/app.php
2
3$app->withExceptions(function (Illuminate\Foundation\Configuration\Exceptions $exceptions) {
4 // Register your exception handling here...
5});
This centralizes exception configuration in a single place while preserving the usual $app->make() container bootstrap process.
Configuration
The amount of detail shown in exception pages is controlled by the APP_DEBUG variable (in .env, respected by config/app.php).
true in local/dev—shows stack traces and debug info.
false in production—only basic error messages.
Ensure you never leave APP_DEBUG=true in production, or sensitive data may leak.
Handling Exceptions
Within the single withExceptions closure, you can register:
Reporting Exceptions Log or forward certain exception types differently:
1$app->withExceptions(function ($exceptions) { 2 // Custom report callback for a specific exception 3 $exceptions->report(function (App\Exceptions\InvalidOrderException $e) { 4 // Send to Sentry, Slack, etc. 5 }); 6 // Prevent default logging for this type 7 $exceptions->report(function (App\Exceptions\InternalServiceException $e) { 8 return false; 9 })->stop(); 10});
By default, Laravel still logs everything to your configured channels; use stop() or return false to halt propagation. Global & Exception-Specific Context Add global context to every log entry, or define per-exception context:
1$app->withExceptions(function ($exceptions) { 2 // Global context 3 $exceptions->context(fn () => ['hostname' => gethostname()]);// Per-exception context 4 // In App\Exceptions\InvalidOrderException: 5 public function context(): array 6 { 7 return ['order_id' => $this->orderId]; 8 } 9});
This enriches logs for easier debugging and monitoring.
Exception Log Levels Adjust severity per exception type:
1use Psr\Log\LogLevel; 2use PDOException; 3$app->withExceptions(function ($exceptions) { 4 $exceptions->level(PDOException::class, LogLevel::CRITICAL); 5});
Now database errors can trigger your critical‐level alerts or channels.
Ignoring Exceptions by Type Laravel automatically ignores 404s, 419s, etc. To stop ignoring or to add new types:
1use Symfony\Component\HttpKernel\Exception\HttpException; 2$app->withExceptions(function ($exceptions) { 3 // Stop ignoring legitimate HttpExceptions 4 $exceptions->stopIgnoring(HttpException::class); 5});
Or implement Illuminate\Contracts\Debug\ShouldntReport on your custom exception to never log it.
Rendering Exceptions Control the HTTP response for specific exceptions:
1use Illuminate\Http\Request; 2use App\Exceptions\InvalidOrderException; 3$app->withExceptions(function ($exceptions) { 4 $exceptions->render(function (InvalidOrderException $e, Request $request) { 5 return response()->view('errors.invalid-order', status: 500); 6 }); 7 // Override Symfony’s 404 for API routes 8 $exceptions->render(function (Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e, Request $request) { 9 if ($request->is('api/*')) { 10 return response()->json(['message' => 'Resource not found.'], 404); 11 } 12 }); 13});
If your render callback returns null, Laravel falls back to default rendering. Rendering as JSON Customize when JSON should be used:
1use Throwable; 2$app->withExceptions(function ($exceptions) { 3 $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { 4 // Always JSON for admin routes: 5 return $request->is('admin/*') || $request->expectsJson(); 6 }); 7});
This replaces the default “Accept header” check when deciding JSON vs HTML.
Full Response Customization For rare cases, tweak the final response object:
1use Symfony\Component\HttpFoundation\Response; 2$app->withExceptions(function ($exceptions) { 3 $exceptions->respond(function (Response $response) { 4 if ($response->getStatusCode() === 419) { 5 return back()->with('message', 'Session expired, please try again.'); 6 } 7 return $response; 8 }); 9});
Reportable & Renderable Exception Classes
Rather than defining everything in bootstrap/app.php, you can place logic directly on exception classes:
1namespace App\Exceptions;
2use Exception;
3use Illuminate\Http\Request;
4use Illuminate\Http\Response;
5class PaymentFailedException extends Exception
6{
7 public function report(): void
8 {
9 // Custom reporting (e.g., notify billing team)
10 }
11 public function render(Request $request): Response
12 {
13 return $request->expectsJson()
14 ? response()->json(['error' => 'Payment failed.'], 402)
15 : response()->view('errors.payment-failed', [], 402);
16 }
17}
If you return false from render(), Laravel uses its default renderer.
HTTP Exceptions & Error Pages
Publishing & Customizing Error Views Laravel’s built-in error templates can be published and fully customized:
1php artisan vendor:publish --tag=laravel-errors
This copies views into resources/views/vendor/errors, where you can edit 404.blade.php, 500.blade.php, etc. Fallback Error Pages
Define resources/views/errors/4xx.blade.php or 5xx.blade.php as fallbacks for all 4xx/5xx statuses not specifically handled. (404, 500, 503 have dedicated templates.)
Best Practices
Centralize: Keep all exception registration in bootstrap/app.php via withExceptions.
Consistent API: Ensure all JSON errors follow a uniform structure.
Security: Mask sensitive details in production (APP_DEBUG=false).
Monitoring: Leverage report() and custom channels for critical alerts.
Documentation: List your custom exception types, response formats, and published error views in your project README.
By using Laravel 12’s withExceptions API plus exception-level report/render methods, you gain full control over error reporting, rendering, and user feedback—without touching core files. Implement these patterns to make your application’s error handling robust, maintainable, and tailored to your needs.