skip to content

In Laravel, how do you give a web app branded error pages for 404 and other HTTP errors, and how do the 4xx and 5xx fallback views work?

level: juniorimportance: must knowfreq 62%

answer

  1. views named after the status code
  2. resources/views/errors/404.blade.php
  3. $exception passed into the view
  4. 4xx/5xx only where no page exists
  5. vendor:publish --tag=laravel-errors

basics

~20 s

Create resources/views/errors/404.blade.php (any status code works); Laravel renders it for an HTTP exception with that status and passes it as $exception. 4xx.blade.php and 5xx.blade.php only cover statuses with no specific page, yours or the framework's.

solid answer

~40 s

For an HTML response, Laravel's handler looks up a view named after the HTTP status in the `errors` view namespace: first `resources/views/errors/{status}.blade.php`, then the framework's own error pages, then `errors/4xx` or `errors/5xx`, and finally a plain Symfony page. So `resources/views/errors/404.blade.php` brands every 404. The view receives the `HttpException` as `$exception` and an empty `$errors` bag. The framework ships pages for 401, 402, 403, 404, 419, 429, 500 and 503, so a `4xx.blade.php` fallback only takes effect for other codes such as 400, 405 or 410 unless you override those individually. `php artisan vendor:publish --tag=laravel-errors` copies the framework's templates, including its layout, for editing. In production a non-HTTP exception is wrapped in a 500 `HttpException` carrying the original message, so a 500 page must not echo `$exception->getMessage()`.

code

html · 9 lines
html
@extends('layouts.airline')

@section('title', 'Page not found')

@section('content')
    <h1>We couldn't find that page</h1>
    <p>{{ $exception->getMessage() ?: 'The flight or page you asked for may have moved.' }}</p>
    <a href="{{ url('/') }}">Back to departures</a>
@endsection

go deeper

for a junior

Know that resources/views/errors/404.blade.php brands every 404 and that the view gets the exception as $exception.

for a middle

Explain the lookup order, why the framework's shipped pages beat 4xx/5xx fallbacks, and when JSON or debug mode skip the views.

for a senior

Keep internal messages off 5xx pages and make error layouts safe to render when the database or session is down.

for a principal

Own a consistent error experience across web and API surfaces, including which status codes get branded pages.

## How Laravel picks an error page When an exception reaches Laravel's exception handler during a web request and nothing custom renders it, the handler produces an **HTML error page** unless the request should get JSON. For that page it works with an **HTTP exception**: an instance of Symfony's `HttpExceptionInterface`, which carries a status code and headers. `abort(404)`, an unknown URL and a missing route-bound model all end up as one. Any other exception is, in production, wrapped in an `HttpException` with status **500**. The handler then registers the `errors` view namespace, pointing first at `resources/views/errors` in your app and then at the framework's built-in error views, and searches in this order: 1. `errors::{status}`, for example `errors::404`, in your app first and the framework second. 2. `errors::{first digit}xx`, for example `errors::4xx`. 3. If neither exists, a minimal page rendered by Symfony's error renderer. ## Creating a branded page Add a Blade file named after the status code: - `resources/views/errors/404.blade.php` for missing pages and records; - `resources/views/errors/403.blade.php` for forbidden actions; - `resources/views/errors/500.blade.php` for server faults; - `resources/views/errors/503.blade.php` for unavailable service. Each view receives two variables: **`$exception`**, the `HttpException` being rendered, and **`$errors`**, an empty `ViewErrorBag` so layouts that print validation errors do not break. The page is returned with the exception's status code and headers, so a branded 404 is still a real 404 for crawlers and monitors. To start from Laravel's own design rather than a blank file, publish the templates: ```bash php artisan vendor:publish --tag=laravel-errors ``` This copies the framework's error views, including the shared `layout` and `minimal` templates, into `resources/views/errors`, where your copies win. ## Sharing one design across pages The framework's own pages are tiny: `404.blade.php` just extends `errors::minimal` and fills three sections, `title`, `code` and `message`, using translated strings such as `__('Not Found')`. After publishing, you can restyle `minimal.blade.php` (or `layout.blade.php`) once and every status page picks up the airline's branding, while each status file keeps its own wording. Because the strings go through `__()`, adding them to your JSON translation files localises the pages without touching the templates. ## Fallback pages: 4xx and 5xx `4xx.blade.php` and `5xx.blade.php` catch statuses in that range that have no specific page. The catch is the lookup order: the framework's own pages count as specific pages. Laravel 13 ships views for these codes: | Code | Meaning | |---|---| | 401 | Unauthorized | | 402 | Payment required | | 403 | Forbidden | | 404 | Not found | | 419 | Page expired | | 429 | Too many requests | | 500 | Server error | | 503 | Service unavailable | So a lone `4xx.blade.php` will not restyle a 404 or a 403; it applies to codes such as 400, 405, 409 or 410. Likewise `5xx.blade.php` applies to 502 or 504 but not to 500 or 503. To brand everything, create the specific files too, or publish the templates and restyle their shared layout. ## What not to print on an error page The wrapping step matters for 500 pages. A `QueryException` thrown in production becomes `new HttpException(500, $e->getMessage(), $e)`, so `$exception->getMessage()` in `500.blade.php` prints the database error to the visitor. Print your own copy on 5xx pages and use the message only on 4xx pages where you wrote it yourself, for example through `abort(404, 'Flight not found.')`. ## When the views are not used - **JSON requests:** if the request should get JSON, the handler returns a JSON body instead and never touches the views. - **Debug mode:** with `APP_DEBUG=true`, a non-HTTP exception shows the development exception page rather than `500.blade.php`; HTTP exceptions still use the status views. - **Custom rendering:** a render callback or an exception's own `render()` method that returns a response bypasses the views. - **A broken error view:** if the view itself throws, the handler rethrows in debug mode; otherwise it reports the new error and falls back to the plain Symfony page. ## Common mistakes - Relying on `4xx.blade.php` to style 404s. - Echoing `$exception->getMessage()` on the 500 page. - Testing error pages only with `APP_DEBUG=true`, where server faults never reach `500.blade.php`. - Extending a layout that needs a logged-in user or a database query, which can fail on the very error page that reports a database outage.

  • Why does a custom 4xx.blade.php not change the 404 page?
    The handler looks for `errors::404` before `errors::4xx`, and the `errors` namespace includes the framework's own views, which ship a 404 page. That specific page is found first. Create `404.blade.php` yourself, or publish the templates and edit them.
  • Why is printing $exception->getMessage() on a 500 page risky?
    In production a non-HTTP exception is wrapped in a 500 `HttpException` that copies the original message. A database or third-party error message then reaches the visitor. Show fixed copy on 5xx pages.
  • Does a branded 404 page still return status 404?
    Yes. The view is returned with the exception's status code and headers, so clients, crawlers and monitors still see 404; only the body is yours.

saying these in an interview costs you the question

  • A 4xx.blade.php fallback also restyles the 404 and 403 pages
  • Error views must be registered in a route or provider
  • A branded error view is returned with status 200
  • Printing $exception->getMessage() on the 500 page is always safe
  • Error pages also render for JSON API requests