How To Create Custom Error Pages in Laravel

May 31st, 2024 • Last updated 3 minutes read time

Laravel's default error pages (like a 404) are great during development, but when it's time to push your app to production, don't leave your error pages as the defaults. Here's how to quickly overwrite them and build your own.

Let's look at how to add a custom 404 page to Laravel, and then we'll dive into how this works behind the scenes.

To add a custom 404 page to Laravel, just create a file in your views directory under an errors directory. For a 404, you'd create a resources/views/errors/404.blade.php file.

Here's an example of the content of that custom 404:

<div>
  Oops, page not found
</div>

Pretty simple content, but when hitting a page that doesn't exist, you'll now just see 'Oops, page not found' instead of the default that ships with Laravel.

This method works for most other status codes, too.

The same principle applies here. Just take the status code of the error (which will be displayed on the default Laravel error page) and create a new file.

I do this for CSRF errors (419) since it's helpful to let the user know what went wrong and what they need to do.

Once again, here's an example.

resources/views/errors/419.blade.php

The page expired while you were trying to perform that action. Go back, refresh, a try again.

I'm sure you get the idea now. For any error you'd like to customise, take the HTTP status code and create a corresponding Blade file with whatever you want to display instead.

As a bonus, let's source dive to see how Laravel handles this.

Under the framework's base Handler class, there's a method that handles HTTP exceptions.

class Handler implements ExceptionHandlerContract {
    protected function renderHttpException(HttpExceptionInterface $e)
    {
        $this->registerErrorViewPaths();

        //...
    }

    //...
}

The registerErrorViewPaths method looks like this.

protected function registerErrorViewPaths()
{
    (new RegisterErrorViewPaths)();
}

There is not much going on here, but if we look at what happens when the RegisterErrorViewPaths class is invoked, we see this.

public function __invoke()
{
    View::replaceNamespace('errors', collect(config('view.paths'))->map(function ($path) {
        return "{$path}/errors";
    })->push(__DIR__.'/views')->all());
}

While this registers the errors namespace for views, it doesn't take care of actually rendering them. Now that Laravel has a namespace for errors, though, here's where the magic happens, again, inside the Handler class.

protected function getHttpExceptionView(HttpExceptionInterface $e)
{
    $view = 'errors::'.$e->getStatusCode();

    if (view()->exists($view)) {
        return $view;
    }

    //...

    return null;
}

The getHttpExceptionView method grabs any views you've created under the new errors namespace.

Finally, here's how it's actually rendered.

protected function renderHttpException(HttpExceptionInterface $e)
{
    $this->registerErrorViewPaths();

    if ($view = $this->getHttpExceptionView($e)) {
        try {
            return response()->view($view, [
                'errors' => new ViewErrorBag,
                'exception' => $e,
            ], $e->getStatusCode(), $e->getHeaders());
        } catch (Throwable $t) {
            config('app.debug') && throw $t;

            $this->report($t);
        }
    }

    return $this->convertExceptionToResponse($e);
}

Take a good look at this method. You can see how everything we've discussed comes together to render the view.

While this might seem unnecessarily broken up, it's for a good reason. We can override any logic within the Handler class in the framework skeleton!

If you found this article helpful, you'll probably love our practical screencasts!
Author
Alex Garrett-Smith

Comments

No comments, yet. Be the first!