webfactory/exceptions-bundle
Symfony bundle for handling exceptions through controller actions. Map exception classes to controllers to render custom error pages, log or transform errors, and use the full framework stack instead of static Twig templates.
This is a Symfony bundle, not a Laravel package. It will not install or boot in a Laravel application — there is no service provider, and its classes extend Symfony's Controller/Bundle base types from the Symfony 2.x era. Treat it as a reference implementation of user-friendly error-page patterns, not as a dependency.
If you are evaluating it inside a Symfony app:
composer require webfactory/exceptions-bundle
Then register Webfactory\ExceptionsBundle\WebfactoryExceptionsBundle in app/AppKernel.php (or config/bundles.php on Symfony 4+). Look first at the bundle's Controller/ directory (the dev-mode exception controller) and Resources/views/ (the Twig error templates) — those two pieces are the whole point of the package.
First use case in Laravel is not "install this" but "copy the idea": a dev-only route that renders your real error pages so designers can review them without breaking the app. Laravel already gives you the substrate — resources/views/errors/{code}.blade.php, App\Exceptions\Handler, and config('app.debug') — so the bundle's value to you is conceptual.
Concretely, the Laravel equivalent of its feature set is:
// resources/views/errors/500.blade.php — production error page
// app/Exceptions/Handler.php — routing exceptions to views/JSON
// Ignition / Whoops — debug-mode stack traces
Separate presentation from handling. The bundle's core idea is that exception handling (log, report, decide status code) and exception rendering (which template, what tone) are different concerns. In Laravel this maps cleanly:
// app/Exceptions/Handler.php
public function register(): void
{
$this->renderable(function (PaymentDeclinedException $e, Request $request) {
return $request->expectsJson()
? response()->json(['error' => 'Payment declined'], 402)
: response()->view('errors.payment', ['e' => $e], 402);
});
}
Content negotiation is the load-bearing pattern. The Symfony controller in the bundle picks HTML vs. other formats based on the request. Laravel's expectsJson() does the same — use it consistently so API clients never receive rendered HTML.
Per-status-code templates. Laravel resolves resources/views/errors/404.blade.php, 403, 419, 500, etc. automatically. This is the bundle's "building blocks" model implemented natively. Add a fallback errors/500 and never let a missing template cause a second exception.
Dev-mode error preview controller. Mirror the bundle's dev controller with a guarded route:
// routes/web.php
if (app()->isLocal()) {
Route::get('/_errors/{code}', fn (int $code) => response()
->view("errors.{$code}", [], $code));
}
This lets you QA error pages with APP_DEBUG=false locally (which you should test, since debug mode bypasses your views entirely).
Exception classes that render themselves. Laravel supports render() on the exception — a cleaner version of what the bundle's controller did centrally, because the mapping lives next to the exception:
class PaymentDeclinedException extends Exception
{
public function render(Request $request)
{
return $request->expectsJson()
? response()->json(['error' => 'Payment declined'], 402)
: response()->view('errors.payment', [], 402);
}
}
Translation and tone. The bundle's templates are built around human-readable copy. Keep message text in lang/en/errors.php, not in Blade, so the same page can be localized — and so product can edit wording without touching templates.
composer.json even in a Symfony project without auditing; in Laravel it's a non-starter.ErrorHandler plus Ignition/Whoops in debug. Any attempt to layer this bundle's controller on top will produce blank pages or handler-order surprises.auth()->user(), no @include of partials that touch the session or cache. A throwing error view yields a bare 500 with no body.APP_DEBUG=true in production leaks stack traces and bypasses your templates entirely — the exact problem this bundle was written to solve. Verify with php artisan config:clear after changing it; config caching will mask the change.php artisan view:clear. Blade caches aggressively and you will otherwise stare at your old template.ModelNotFoundException and AuthenticationException already map to 404/redirect. Don't re-render them in Handler::render or you'll override framework behavior.errors.* views (override wholesale) and renderable()/reportable() callbacks (fine-grained), which is a strictly better separation.$this->get('/missing')->assertNotFound()->assertSee('Page not found') and a forced-500 test are the only way to catch a broken error page before your users do.How can I help you explore Laravel packages today?