Custom error pages in Laravel: 404, 500, 419, and 403

- Andrés Cruz - ES En español

Video thumbnail

When you start working with Laravel and already have your project up and running —routes, controllers, even Vue if you use it on the frontend— there is one detail that almost always gets left for last… until the first 404 appears in production: custom error pages.

In my case, at first I used to leave Laravel's default pages. They worked, yes, but they completely broke the user experience and didn't fit the project's design. Luckily, Laravel makes it very easy: you can customize error pages using Blade without touching routes or controllers, giving you full control over what the user sees when something goes wrong.

In this article, I explain how to create custom error pages in Laravel —from the very basics to real-world production best practices— covering 404, 500, 419, and 403 errors.

Pro-Tip:

Customizing the view is great for the user, but never forget the backend. If you create a custom 500 page, make sure your logging system is active and monitorable. Laravel writes errors to storage/logs/laravel.log by default, but that won't alert you in real time. 

A common mistake is showing a nice page to the user while the actual error gets lost in the log without anyone seeing it. If you want to take it to the next level, I recommend integrating tools like Sentry or Flare to receive notifications whenever a user hits that 500 page. That is what separates a professional application from a practice project.

Now that we know how to configure Vue and Laravel, let's move on to the next step.

Why customize error pages in Laravel?

Laravel includes default error pages that work fine during development, but in production they present several issues:

  • They show a generic design that breaks the project's visual identity
  • They do not follow your application's layout or styles
  • They offer the user no option to continue browsing
  • They project an unprofessional image that can undermine user trust

Customizing these pages allows you to:

  • Maintain visual consistency with the rest of the site
  • Improve user experience (UX) even when something fails
  • Avoid exposing sensitive technical information to the end user
  • Guide the user toward a clear action (return to home, contact support, etc.)

And the best part: for most cases, you don't need to touch any routes or controllers.

Where and how Laravel manages error pages

Laravel automatically detects custom error pages. You just need to create the corresponding views in the directory:

resources/views/errors/

The name of each file must match the HTTP status code you want to handle. For example:

resources/views/errors/404.blade.php
resources/views/errors/500.blade.php

How it works is very straightforward:

  • The filename must match the HTTP status code of the error
  • If the file exists, Laravel renders it automatically without additional configuration
  • There is no need to register routes or modify any controller

Quick reference of files by error code:

  • 404.blade.php → Page not found
  • 500.blade.php → Internal server error
  • 403.blade.php → Access forbidden
  • 419.blade.php → Page expired (CSRF token)

Just by creating the right file, Laravel does all the work.

How to create custom error pages in Laravel

Proper structure for resources/views/errors

If the folder does not exist yet in your project, create it manually:

resources/views/errors/

Inside, add the files corresponding to the errors you want to customize. There is no limit: you can cover as many HTTP status codes as you need.

⚠️ If the view exists but is empty, Laravel will display a blank page. Always ensure the file contains content, at least the minimum Blade structure with @extends and @section.

Example of a custom 404 page in Laravel

File: resources/views/errors/404.blade.php

@extends('layouts.app')
@section('content')
<div class="container">
   <h1>404 - Page Not Found</h1>
   <p>Sorry, the page you are looking for does not exist or has been moved.</p>
   <a href="{{ url('/') }}">Back to Home</a>
</div>
@endsection

I always add a clear link to return to the home page: a dead-end 404 is a bad experience that can lead to an unnecessary bounce. If your application has a search bar or a category menu, it is also a good idea to include them here.

Example of a custom 500 page in Laravel

File: resources/views/errors/500.blade.php

@extends('layouts.app')
@section('content')
<div class="error-page">
   <h1>500 - Internal Server Error</h1>
   <p>Something went wrong on our end. Please try again in a few minutes.</p>
   <a href="{{ url('/') }}">Back to Home</a>
</div>
@endsection

⚠️ The custom 500 page will only be displayed when APP_DEBUG=false.
This confused me quite a bit at first because I could never see my custom view locally. Below I explain how to test it without needing to deploy to production.

Example of a custom 419 page in Laravel (expired CSRF token)

The 419 error is unique to Laravel and occurs when a form's CSRF token has expired or does not match. It is very common in forms with long-duration sessions or when a user leaves a tab open for a long time.

File: resources/views/errors/419.blade.php

@extends('layouts.app')
@section('content')
<div class="container text-center">
   <h1>419 - Page Expired</h1>
   <p>Your session has expired. Please refresh the page and try again.</p>
   <a href="{{ url()->previous() }}">Go back to the previous page</a>
</div>
@endsection

The 419 error is one of the most frequent in applications with forms and, at the same time, one that confuses users the most if not clearly explained. A message like "Your session has expired, please reload the page" is infinitely better than showing a generic error without context.

Summary

  • You can customize errors from the 4xx series (such as 401, 402, 403, 404, 405, 419) and the 5xx series.
  • Customization is done by creating the resources/views/errors/ folder and the corresponding files named after the HTTP status code.
  • Just by creating the file, Laravel will automatically display that view whenever the error occurs.
  • It is important that the file is not empty. You can include styles, Blade components, or any design you already use in your project.
  • To see the custom 500 page, you must ensure Laravel is in production mode (APP_DEBUG=false).
  • To customize other codes, simply create files like 401.blade.php, 403.blade.php, 419.blade.php, etc.
  • With this, you will have complete control over the appearance of your error pages and significantly improve the user experience.

Other HTTP error codes in Laravel (401, 403, 405…)

You can customize any HTTP status code simply by creating the corresponding file in resources/views/errors/:

  • 401.blade.php → Unauthorized (user is not authenticated)
  • 403.blade.php → Forbidden (user is authenticated but lacks permissions)
  • 405.blade.php → Method Not Allowed

Example of a custom 403 page:

@extends('layouts.app')
@section('content')
<div class="container text-center">
   <h1>403 - Access Denied</h1>
   <p>You do not have permission to access this section.</p>
   <a href="{{ url('/') }}">Back to Home</a>
</div>
@endsection

In applications with roles and permissions —for example using Laravel's Gates or Policies— this file becomes essential. Without it, the user will see an error without any useful explanation.

What happens with debug mode and production mode in Laravel

This point is crucial and is often the cause of 90% of "it's not working" issues.

Laravel controls error handling behavior using the APP_DEBUG environment variable in the .env file:

In development (APP_DEBUG=true):

APP_DEBUG=true
  • Laravel displays its detailed error page with a full stack trace
  • Exceptions, files, and exact code lines are displayed
  • Your custom pages are not displayed in this mode

In production (APP_DEBUG=false):

APP_DEBUG=false
  • Laravel hides all sensitive technical information
  • Your custom views in resources/views/errors/ are rendered
  • Correct, secure, and expected behavior

I usually test error pages by temporarily disabling debug mode locally (APP_DEBUG=false) just to verify that the design looks good. However, remember to re-enable it so you don't lose debugging information during development.

Best practices for error pages in Laravel (UX and maintenance)

Over time I have learned that a good error page should:

  • Use the same layout as the rest of the site (same header, footer, and styles)
  • Explain the problem in plain language, without technical jargon
  • Offer a clear action to the user: go back, reload the page, or contact support
  • Not blame the user: avoid aggressive or cold phrasing

Some concrete ideas you can include in your error pages:

  • Link to home or the main section of your application
  • Button to go back to the previous page using url()->previous()
  • Different messages and designs based on the error type (404, 500, 419)
  • Friendly illustrations or icons to soften the impact of the error

Laravel places no limits here: you can use Blade, components, Tailwind, Bootstrap, or any library you are already using in your project.

How to test your custom error pages in Laravel

The fastest way is to use abort() in a test route. Add this temporarily to your routes/web.php file:

Route::get('/test-404', function () {
   abort(404);
});
Route::get('/test-500', function () {
   abort(500);
});
Route::get('/test-419', function () {
   abort(419);
});
Route::get('/test-403', function () {
   abort(403);
});

Remember to set APP_DEBUG=false in your .env so that your custom views are displayed instead of Laravel's debug screen. Once you have verified the design, remove these test routes before deploying to production.

❓ Frequently asked questions about custom error pages in Laravel

  • Does Laravel automatically detect error pages?
    • Yes. If the file exists in resources/views/errors/, Laravel renders it without any additional configuration.
  • Can I create a generic page for multiple error codes at once?
    • Yes. Laravel also recognizes generic views by series:
      • 4xx.blade.php → fallback for unspecified client errors
      • 5xx.blade.php → fallback for unspecified server errors
  • Is it mandatory to modify Handler.php?
    • No. It is only necessary in advanced cases where you want to execute custom logic before rendering the view (logging the error to an external system, notifying an admin, etc.).
  • Why am I still seeing the default page instead of mine?
    • It is almost always because APP_DEBUG=true. Change that value to false in your .env file and clear the cache using php artisan config:clear.
  • What exactly is the 419 error in Laravel?
    • The 419 error is specific to Laravel and means that the form's CSRF token has expired or is invalid. Laravel uses the VerifyCsrfToken middleware to protect forms against attacks, and when the token check fails, it returns this code. The user solution is to refresh the page and resubmit the form.

Quick reference of error codes in Laravel

CodeMeaningView to createWhen it appears
403Forbidden access403.blade.phpThe user lacks permission to access (roles, Gates, Policies).
404Not found404.blade.phpNon-existent resources: invalid IDs, broken routes, or incorrect URLs.
419Page expired419.blade.phpExpired or invalid CSRF token. Highly recommended to customize this one.
500Server error500.blade.phpSevere execution, database, or configuration issues.

Conclusion

Customizing error pages in Laravel is one of those small improvements that carry a huge impact on the perceived quality of an application. By simply creating the appropriate files in resources/views/errors/, you can take full control over how 404, 500, 419, and 403 errors are displayed and offer a much more professional experience.

Spending a few minutes on these pages prevents confusion, improves project perception, and saves you from giving unnecessary explanations to users when something breaks.

Once this is done, you can move on to more advanced topics… such as working with polymorphic relationships in Laravel, which is usually the next big step.

Learn how to create custom error pages in Laravel using Blade: 404, 500, 419 (CSRF), and 403. No extra configuration required—production-ready.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.