creativeorange/pretty-error-pages
creativeorange/pretty-error-pages provides attractive, customizable error pages for Laravel applications. Replace default exception screens with polished, user-friendly designs that match your brand and improve the experience when errors occur.
This is a small, low-adoption package (0 stars, no dependents) that ships styled Blade views for Laravel's HTTP error statuses so you don't hand-roll a 404/500/503 page. Treat it as "a set of views + a thin service provider," not as infrastructure.
Note on v10.0.0: a new major was tagged, but the release notes are empty. That is not "nothing changed" — it's "unverified." A major bump on a package like this almost always means one of: raised PHP/Laravel floors, dropped illuminate/* versions, a namespace or view-path rename, or a config key rename. Budget ten minutes to diff the tags before you trust it in an existing app.
1. Check what you're actually installing
composer show creativeorange/pretty-error-pages --all # available versions + requires
composer show creativeorange/pretty-error-pages # what you resolved
Read the require block. A ^10.0 that supports Laravel 11/12 is the main reason to take this release; a ^10.0 that still caps at Laravel 10 is a reason to stay on whatever your app already resolves. Then diff the provider and views against the tag you're on:
git clone https://github.com/creativeorange/pretty-error-pages /tmp/pep
cd /tmp/pep && git log --oneline --decorate -20 && git diff <old-tag>..v10.0.0 --stat
2. Install
composer require creativeorange/pretty-error-pages
Laravel will auto-discover the provider via extra.laravel.providers in the package's composer.json. If it doesn't (or you're on an older Laravel), add it manually in config/app.php:
'providers' => [
// ...
CreativeOrange\PrettyErrorPages\PrettyErrorPagesServiceProvider::class,
],
3. Publish the assets
Run the interactive publisher first — it lists the real tags and file targets, which is the fastest way to learn what the package actually provides (and whether v10 renamed anything):
php artisan vendor:publish
Then target it explicitly once you know the tag:
php artisan vendor:publish --provider="CreativeOrange\PrettyErrorPages\PrettyErrorPagesServiceProvider"
# or
php artisan vendor:publish --tag=pretty-error-pages
4. Look here first, in this order
vendor/creativeorange/pretty-error-pages/README.md — the only real source of truth for config keys. Assume it may not have been updated for v10; trust the code over the doc.vendor/creativeorange/pretty-error-pages/src/*ServiceProvider.php — see boot()/register(): what views are registered, what config is merged, whether it hooks View::composer or just publishes.vendor/creativeorange/pretty-error-pages/resources/views/ — the actual markup you'll be restyling.config/pretty-error-pages.php (after publishing) — branding, colors, which codes to cover.5. First use case: a branded 404
php artisan vendor:publish --tag=pretty-error-pages
php artisan view:clear
Add a throwaway route and hit it:
Route::get('/boom', fn () => abort(404));
Confirm you see the package's page rather than Laravel's default grey "Not Found" screen. If not, jump to the Gotchas section — it's almost always APP_DEBUG or view resolution.
Let Laravel's native resolution do the work. The most maintainable integration with any package like this is to end up with resources/views/errors/404.blade.php, 500.blade.php, 503.blade.php in your app. Laravel's exception handler resolves those paths first, so you own the views and the package becomes optional. Copy the published views in, then keep or drop the dependency. This is also your insurance against v10 changing the markup out from under you.
Extend, don't duplicate. If the package ships a shared layout (e.g. errors/layout.blade.php), have each status view @extends it and override only the copy:
{{-- resources/views/errors/404.blade.php --}}
@extends('errors.layout')
@section('code', '404')
@section('title', 'Page not found')
@section('message', 'That link is dead. Try the homepage or search.')
@section('action')
<a href="{{ url('/') }}" class="btn">Back to home</a>
@endsection
Drive it from config, not hardcoded strings. Keep support emails, brand colors, and CTA URLs in config/pretty-error-pages.php (or your own config/errors.php) so staging and production can differ without touching Blade:
// config/pretty-error-pages.php
return [
'brand' => env('APP_NAME'),
'support_email' => env('SUPPORT_EMAIL', '[email protected]'),
'home_url' => '/',
'show_code' => true,
];
Re-publish the config after a major bump and diff it against yours before merging — a renamed key silently falls back to the package default, which is how you end up shipping a placeholder email.
Preview all statuses in one pass. Don't hunt for real failures — register a dev-only preview route:
if (app()->environment('local')) {
Route::get('/_errors/{code}', function (int $code) {
abort($code);
})->where('code', '4\d\d|5\d\d');
}
Test the pages as part of the suite. These regress silently because nobody visits a 500 page on purpose:
public function test_missing_page_renders_branded_404(): void
{
$this->get('/definitely-not-a-route')
->assertStatus(404)
->assertSee(config('pretty-error-pages.support_email'));
}
public function test_maintenance_page_renders(): void
{
$this->get('/')
->assertStatus(503)
->assertSee('back soon');
}
If you upgrade to v10, run exactly these two tests before and after. They catch a view-path rename faster than reading a diff.
Wire it into your own chrome sparingly. Extending your app's layouts.app looks nice in staging but couples the error page to middleware, session, and auth — all of which can be exactly what's broken. Prefer a standalone, self-contained layout for 500/503, and allow the full app layout only for 404.
APP_DEBUG=true bypasses everything. In debug mode Laravel renders Ignition/Whoops, not your error views. You will "confirm" the package works by setting APP_DEBUG=false locally — do that before you conclude it's broken or before you file the v10 upgrade as "it did nothing."
v10.0.0 shipped with no release notes. That changes your upgrade procedure, not your code. Concretely: pin the exact version ("creativeorange/pretty-error-pages": "10.0.0"), diff src/ and resources/views/ against your previous tag, check the require block for PHP and illuminate/* floors, and run your error-page tests. If the diff is only a Laravel-version constraint bump, take it and move on. If the views or config keys moved, publish into a scratch copy first and compare, don't publish over your edited files.
Don't upgrade mid-incident. Major bump, empty changelog, zero dependents — nothing external has validated this release. Do it on a branch, on a boring Tuesday.
Error pages must not touch the database, session, auth, or Vite. A 500 frequently means the DB or cache is down; a 503 is served during php artisan down, before most of the app boots. Inline the CSS (or use a CDN) and use url()/config() only. Never @vite, never auth()->user(), never an Eloquent query in a 503 view. Re-audit this after any upgrade — a new release can quietly add a @vite or a component that resolves through the container.
503 is special. It's rendered by the maintenance-mode handler, so route through the standard errors/503.blade.php path and keep it dependency-free. Also add resources/views/errors/503.blade.php manually if the publisher skips it — many packages only ship 404/500, and it's worth confirming what v10 actually publishes now.
php artisan view:clear after every publish or config change. Blade caches compiled views; a stale cache is the #1 "I published it and nothing changed" cause. Follow with config:clear if you edited the config file. On the v10 upgrade specifically, run view:clear before you evaluate whether anything changed — otherwise you'll diff compiled output against your memory.
Don't leak environment details. Audit the published views for {{ $exception->getMessage() }} or {{ config('app.env') }} before shipping. On a 500 page, print a generic message plus a correlation ID, and log the real exception. Re-check this on every major — new markup is exactly where a stray $exception dump sneaks in.
Adoption risk is still the real gotcha. One new tag with no notes does not change the picture: 0 stars, no dependents, no evidence anyone else is running v10. Assume no upstream fixes and no guaranteed forward compatibility with Laravel 13. The pragmatic move for most teams remains: publish the views, copy them into resources/views/errors/, commit them, and composer remove creativeorange/pretty-error-pages. You get the design with zero dependency surface — and the v10 release becomes irrelevant to you.
Override precedence works in your favor. Anything in resources/views/errors/ wins over the package's namespace, and a published config wins over the package default. So every customization point is "publish and edit" — there is rarely a need to fight the provider. If v10 renamed a view namespace, your local overrides keep working anyway.
Extension points, in order of usefulness: (1) published views in resources/views/errors/, (2) published config, (3) a Blade component or View::composer('errors.*', ...) to inject shared data like support links, (4) subclassing/overriding the service provider — only if you're already forking.
Localization is on you. If you serve multiple locales, add resources/views/errors/{code}.blade.php with __('errors.404') keys rather than string literals; the package's copy won't be translated.
How can I help you explore Laravel packages today?