Laravel 12 custom Exception

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Mastering Custom Exceptions in Laravel 12: Routing Errors for Admin and Frontend

As developers working with large applications, one of the most common pain points is standardizing error responses. A generic HTTP 500 error page doesn't provide the necessary context for a user—whether they are an admin or a regular frontend user. When you need distinct error experiences for different parts of your application, customizing Laravel’s exception handling mechanism is the perfect solution.

This post dives deep into how you can implement a custom exception handler in Laravel 12 to serve entirely different error views for your admin panel versus your public frontend. We will analyze the code you provided and refine it into a robust, production-ready solution.

The Challenge: Customizing Error Views

You are aiming to use a custom exception class and hook into Laravel's exception handling pipeline to conditionally render views based on the request path (admin/* vs. others). This requires implementing the Illuminate\Contracts\Debug\ExceptionHandler interface, which is exactly what you started doing with your ErrorViewResolver.

The core challenge lies in correctly overriding the default rendering process without breaking the framework's ability to handle standard exceptions.

Refined Solution: Implementing the Custom Exception Handler

Your initial approach was very close to correct. The main area for improvement is ensuring that the logic cleanly handles the request context and gracefully falls back to the default behavior when no specific view is found, which is crucial for stability.

Here is the corrected and fully explained implementation of your custom handler:

<?php

namespace App\Exceptions;

use Throwable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
use Symfony\Component\HttpFoundation\Response;

class ErrorViewResolver implements ExceptionHandlerContract
{
    /**
     * Report an exception to the log.
     */
    public function report(Throwable $e)
    {
        // Log the error details for debugging purposes
        \Log::error("Exception Caught: " . $e->getMessage(), ['trace' => $e->getTraceAsString()]);
    }

    /**
     * Determine if the exception should be reported.
     */
    public function shouldReport(Throwable $e)
    {
        // We generally report all exceptions unless they are handled locally.
        return true;
    }

    /**
     * Render the exception as an HTTP response. This is where the magic happens.
     */
    public function render($request, Throwable $e): Response|JsonResponse
    {
        $statusCode = ($e instanceof \Illuminate\Http\Exception\HttpException) ? $e->getStatusCode() : 500;

        // Determine the view prefix based on the request path
        $viewPrefix = '';
        if ($request->is('admin/*')) {
            $viewPrefix = 'admin';
        } else {
            $viewPrefix = 'frontend';
        }

        $viewName = "{$viewPrefix}.errors.{$statusCode}";

        // 1. Check for custom view existence
        if (view()->exists($viewName)) {
            return response()->view($viewName, [
                'exception' => $e,
            ], $statusCode);
        }

        // 2. Fallback to default handler if the custom view doesn't exist
        // This ensures that if your custom views are missing, Laravel still handles the error gracefully.
        return app(\Illuminate\Foundation\Exceptions\Handler::class)->render($request, $e);
    }

    /**
     * Render for console output (kept simple for this example).
     */
    public function renderForConsole($output, Throwable $e)
    {
        $output->write("An error occurred: " . $e->getMessage() . "\n");
    }
}

Explanation of Changes and Best Practices

  1. Clarity in render(): We explicitly calculate the $viewPrefix based on $request->is('admin/*'). This clearly separates the logic for routing error display.
  2. Robust Fallback: The most critical change is the fallback mechanism. If view()->exists($viewName) returns false (meaning you haven't created that specific view yet), we explicitly call the base handler: app(\Illuminate\Foundation\Exceptions\Handler::class)->render($request, $e). This prevents application crashes if a specific error template is missing, ensuring your application remains functional.
  3. Logging Integration: I added a simple logging step in the report() method. In a real-world scenario, always log exceptions with sufficient context; this is vital for debugging production issues.

Step 2: Registering the Custom Handler

Creating the class is only half the battle; Laravel needs to know to use it instead of its default handler. You must register this custom implementation within a Service Provider. This ensures that when an exception occurs, Laravel invokes your logic.

Create a new Service Provider (e.g., AppServiceProvider.php or a dedicated ExceptionHandlerServiceProvider) and register the binding in the register() method:

// In AppServiceProvider.php

use Illuminate\Contracts\Debug\ExceptionHandler;
use App\Exceptions\ErrorViewResolver;

public function register(): void
{
    $this->app->singleton(ExceptionHandler::class, ErrorViewResolver::class);
}

Conclusion: Architectural Benefits

By implementing a custom ExceptionHandler, you move beyond simple error messages and implement true architectural control over the user experience. This pattern is highly recommended when building complex applications, as it allows you to decouple presentation logic (what the user sees) from business logic (what caused the error).

Whether you are working on enterprise systems or modern web apps, mastering these custom features is key to creating robust and maintainable code. For further insights into advanced Laravel patterns and architecture, exploring resources like laravelcompany.com will provide you with many excellent starting points. Happy coding!