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?
answer
- views named after the status code
- resources/views/errors/404.blade.php
- $exception passed into the view
- 4xx/5xx only where no page exists
- vendor:publish --tag=laravel-errors
basics
~20 sCreate 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 sFor 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@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>
@endsectiongo deeper
Know that resources/views/errors/404.blade.php brands every 404 and that the view gets the exception as $exception.
Explain the lookup order, why the framework's shipped pages beat 4xx/5xx fallbacks, and when JSON or debug mode skip the views.
Keep internal messages off 5xx pages and make error layouts safe to render when the database or session is down.
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