How To Add Custom Error Pages to Laravel

May 31st, 2024

The default error pages (like a 404) are great for development, but when it's time to push your app to production, don't leave your error pages as the defaults.

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 (and any other supported status code) page to Laravel, just create a file in your views directory under an errors directory.

Here's an example.


Oops, page not found

Pretty simple content, but when hitting a page that doesn't exist, you'll see 'Oops, page not found'.

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.


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)



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";

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)

    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;


    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!

Thanks for reading! If you found this article helpful, you might enjoy our practical screencasts too.
Alex Garrett-Smith
Share :


No comments, yet. Be the first to leave a comment.