I don't think you need to make a full implementation of the ExceptionHandler contract to achieve your logic. In laravel 11, you'd only need to register a renderable callback inside the Handler (located in app/Exceptions/Handler.php)
<?php
// ./app/Exceptions/Handler.php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class Handler extends ExceptionHandler
{
...
public function register(): void
{
$this->renderable(function (HttpException $ex) {
$statusCode = $ex->getStatusCode();
$view = $request->is('admin/*')
? "admin.errors.{$statusCode}"
: "frontend.errors.{$statusCode}";
if (view()->exists($view)) {
return response()->view($view, ['exception' => $ex], $statusCode);
}
});
$this->renderable(function (Throwable $th) {
$view = $request->is('admin/*')
? 'admin.errors.500'
: 'frontend.errors.500';
if (view()->exists($view)) {
return response()->view($view, ['exception' => $th], 500);
}
});
}
}
In laravel 12, that logic has been moved to the bootstrap/app.php file, but the syntax should largely remain the same.
<?php
// ./bootstrap/app.php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
// or $exceptions->renderable if you prefer to keep the syntax as close to 11 as possible. The render and renderable methods do the exact same thing.
$exceptions->render(function (HttpException $ex) {
$statusCode = $ex->getStatusCode();
$view = $request->is('admin/*')
? "admin.errors.{$statusCode}"
: "frontend.errors.{$statusCode}";
if (view()->exists($view)) {
return response()->view($view, ['exception' => $ex], $statusCode);
}
});
$exceptions->render(function (Throwable $th) {
$view = $request->is('admin/*')
? 'admin.errors.500'
: 'frontend.errors.500';
if (view()->exists($view)) {
return response()->view($view, ['exception' => $th], 500);
}
});
})->create();
I'll just add one more thing. Just like registering routes, the order in which you register these callbacks matters. The ExceptionHandler just loops through its callbacks and returns the first usable (not null) response.
If you feel this is too bloated or cluttered because you have that much exception handling logic, you can extract the closures into classes.
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render((new RenderHttpExceptions)(...)); // $exceptions->handler->renderable(new RenderHttpException); should also work, because the handler converts non callables to callables.
$exceptions->render((new RenderOtherThrowables)(...));
})->create();
class RenderHttpExceptions
{
public function __invoke(HttpException $ex)
{
...
}
}
class RenderOtherThrowables
{
public function __invoke(Throwable $th)
{
...
}
}
Or just move the whole of callback registering to another class
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
//
})
->withExceptions(function (Exceptions $exceptions) {
ExceptionHandling::registerCallbacks($exceptions);
})->create();
class ExceptionHandling
{
public static function registerCallbacks(Exceptions $exceptions): void
{
$exceptions->render(function (HttpException $ex) { ... });
$exceptions->render(function (Throwable $th) { ... });
}
}