noflash/grumpy-error-bundle
noflash/grumpy-error-bundle provides styled, more approachable error pages for Symfony applications, replacing default exception screens to make debugging clearer and failures less intimidating for users.
What it actually is (read this first): noflash/grumpy-error-bundle is a Symfony bundle, not a Laravel package. It skins Symfony's exception/error pages — the "grumpy" gimmick is a grumpy-cat-style illustration plus snarky copy layered over the standard 404/500 output. It ships as a Symfony Bundle subclass with Twig templates under Resources/views/, wired into Symfony's exception renderer. There is no ServiceProvider, no Illuminate dependency, and nothing composer require will do for a Laravel app beyond pulling in code you cannot boot.
So "getting started" for a Laravel developer is really two paths:
If you just want the effect (99% of you): skip the package, take the idea. Laravel already has first-class custom error pages:
php artisan vendor:publish --tag=laravel-errors
This drops resources/views/errors/{400,401,403,404,419,429,500,503}.blade.php into your app. Replace the contents with your grumpy layout, drop the image in public/images/, and you are done in ten minutes.
If you're reading it for reference/porting: open two places in the repo — the bundle class + DependencyInjection/ extension (to see how it hooks the renderer) and Resources/views/ (to lift the markup and copy). Ignore the Symfony wiring; the templates are the only portable artifact.
First use case to try: a styled 404 only. It's the lowest-risk page, it's the one real users hit, and it lets you validate the asset pipeline and view resolution before you touch 500s.
Layout partial, included everywhere. Don't copy markup into eight Blade files. Make resources/views/errors/layout.blade.php (or errors/partials/grumpy.blade.php) and @include it. Laravel auto-resolves errors/{status}.blade.php by HTTP status code, falling back to errors::minimal — so the partial is your single source of truth.
Gate the novelty by environment. This is the pattern the Symfony bundle can't teach you because it operates at a different layer. In Laravel:
{{-- resources/views/errors/500.blade.php --}}
@if (config('app.debug'))
{{-- let Ignition / Whoops handle it; this view shouldn't render --}}
@endif
@include('errors.partials.grumpy', ['status' => 500])
Better: add config('app.grumpy_errors', true) and check it, so you can kill the joke in one env var during an incident without a deploy of templates.
Hook specific exceptions in the handler, not just status codes, when you want "grumpy" for domain errors:
// bootstrap/app.php (Laravel 11) or App\Exceptions\Handler
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (PaymentDeclinedException $e, $request) {
return response()->view('errors.partials.grumpy', ['status' => 402], 402);
});
})
Mirror the bundle's copy into a lang file. If you want the snark translatable, put strings in lang/en/errors.php and reference __('errors.404.title'). The original bundle hardcodes English in Twig; that's a wart worth fixing on port.
Test it with debug off. The single most common integration bug:
public function test_grumpy_404(): void
{
$this->get('/does-not-exist')
->assertStatus(404)
->assertSee('grumpy', escape: false);
}
For 500s you must set APP_DEBUG=false in the test environment (or config(['app.debug' => false])), otherwise you'll get the debug renderer and your test asserts against the wrong output.
Asset handling. Put the image in public/ and reference with asset('images/grumpy.webp'). Do not serve it from a route, a controller, or a Blade component that could itself throw — an error page that 500s while rendering a 500 is a genuinely fun incident.
Symfony\Component\HttpKernel\Bundle\Bundle in a Laravel app is dead weight. Treat this as "design inspiration," not a dependency. Vendor the image and the copy, not the package.composer.json you have to audit later — the whole value is ~50 lines of markup you can own yourself.APP_DEBUG=true, Laravel renders the debug exception page and your errors/500.blade.php is ignored entirely. If your new page "doesn't show up," this is why. Clear config too: php artisan config:clear.php artisan view:clear after every template edit in a cached environment; otherwise you'll swear the file isn't being read (because it isn't).$exception->getMessage() into the grumpy copy for "helpfulness." In production that's an information-disclosure bug — stack traces, SQL, file paths. Keep production templates static; use report() for the details.419 (CSRF expiry) and 429 (throttling) are far more common in real Laravel apps than 500, and Laravel will render its default page for any status you haven't created. If you style 404 and 500 but not 419, your users will hit the ugly one first.php artisan down awareness. The errors/503.blade.php view is what down serves; if it references assets or a layout that depends on the app booting, it breaks exactly when you need it. Make 503 fully self-contained — inline CSS, no DB, no config() reads that could fail.errors::minimal view is the fallback for any status without a dedicated template. Override that one and you've styled every unmapped error code at once — the cheapest possible version of what this bundle does.How can I help you explore Laravel packages today?