symfony/error-handler
The Symfony ErrorHandler component provides tools to manage PHP errors and ease debugging. Register the error handler and debug class loader, render friendly error pages, and wrap risky code in ErrorHandler::call() to turn failures into exceptions.
Architecture fit. symfony/error-handler is a foundation-layer library that manipulates PHP's global error machinery (set_error_handler, set_exception_handler, register_shutdown_function, error_reporting). It has no service provider, no config file, and no container bindings — integration is via static calls (Debug::enable(), ErrorHandler::call(), FlattenException::createFromThrowable()). That procedural surface is a philosophical mismatch with Laravel's container-first conventions, but a practical fit for two specific jobs: converting silenced PHP warnings into throwables at call sites, and normalizing throwables into a serializable structure for logging/transport.
Critically, this package is almost certainly already in the vendor tree — laravel/framework lists symfony/error-handler as a direct requirement (alongside symfony/console, symfony/http-foundation, symfony/http-kernel). The decision is therefore not "add a dependency" but "use APIs we already ship." That materially lowers the risk profile and should be the first thing we verify (composer why symfony/error-handler).
Integration feasibility. High for narrow uses, low for broad ones.
ErrorHandler::call() wrapping individual risky operations; FlattenException for structured log payloads; DebugClassLoader in CI only; static error-page generation via the dump-static-error-pages command (added in 7.3) for the total-outage case Laravel cannot render.Debug::enable() or ErrorHandler::register(). Laravel's Illuminate\Foundation\Bootstrap\HandleExceptions already owns the error/exception/shutdown handlers and sets error_reporting(-1). Registering Symfony's handler afterwards replaces Laravel's, which disables App\Exceptions\Handler reporting/rendering, Spatie Ignition's debug pages, dontReport, and Laravel's log context enrichment. Two handlers cannot co-own the same globals.Technical risk. Concentrated, not diffuse:
Debug::enable() in a live path leaks stack traces, query bindings, env-derived config, and request args via HtmlErrorRenderer. Must be treated as a hard guardrail, not a code-review convention.ErrorHandler::call().report() both fire, our Sentry/OpsGenie pipeline sees duplicates, inflating incident volume without adding signal.composer require can conflict with laravel/framework's pin. Anchor to Laravel's constraint, never latest.symfony/symfony (issues and PRs live in the monorepo). Real-world adoption is effectively "every Laravel and Symfony application on the planet." The 46.4 composite score and 61.5 opportunity score understate maturity; do not use them as a comparison signal against standalone Laravel packages.Key questions.
symfony/error-handler version does our laravel/framework pin resolve to, and does it include dump-static-error-pages (≥7.3)?Debug::enable() to AppServiceProvider?DebugClassLoader) worth the noise budget, and who triages it?Stack fit. Laravel 10/11+ on PHP 8.2–8.4 is the sweet spot. Adopt the component's utilities, not its lifecycle. Concretely, three integration seams:
ErrorHandler::call() around payment, file I/O, third-party SDK, and queue-job boundaries where PHP warnings are currently silenced by @ and silently degrade behavior. This is per-call, scoped, and reversible.FlattenException::createFromThrowable() inside a custom log processor or the report() path, producing a stable serialized shape (class, message, code, status, trace summary) for Sentry/Bugsnag/OTel. Replaces ad-hoc $e->getTraceAsString() payloads.error_page, and use HtmlErrorRenderer::setTemplate() for a custom generic template. This is the only realistic win for full-stack-down scenarios.Migration path. Non-breaking and incremental, because nothing is being removed. Phase 0: confirm version and current handler ownership (composer why, inspect bootstrap/app.php withExceptions). Phase 1: DebugClassLoader wired into a CI job only, no runtime footprint. Phase 2: convert 5–10 highest-value @-suppressed call sites to ErrorHandler::call() behind feature-flagged jobs; measure retry/fallback rates. Phase 3: introduce FlattenException in the reporting path with duplicate-suppression enabled. Phase 4: static error pages. Only then evaluate whether further adoption is warranted.
Compatibility. MIT-licensed, no transitive surprises (it pulls psr/log and symfony/var-dumper, both already present). Symfony maintains 6.4 LTS, 7.x, and 8.x concurrently with a semiannual minor cadence, so patch drift is continuous but semver-safe. The release notes show frequent PHP-version-coupling fixes — E_DEPRECATED silencing behavior (7.3.1/7.2.8/6.4.23), PHP 8.5 fatal-error backtraces, PHP 8.6 error-message format changes — which is both reassuring (actively maintained) and a reminder that this component tracks PHP internals closely.
Sequencing. Utilities before renderers; renderers before global handlers, and global handlers not at all. Any proposal to ErrorHandler::register() in the request lifecycle should require an ADR and must first resolve the conflict with HandleExceptions — including a documented decision about whether Ignition is retired, which is a much larger product decision than "add a library."
Maintenance. Near-zero incremental cost: the package is already a transitive dependency, patched by routine composer update alongside the framework. The real maintenance burden is our code — each ErrorHandler::call() call site is a small piece of custom control flow that must be understood by whoever reads the stack trace at 3 a.m. Budget code-review attention, not dependency management. The subtree-split repo has no independent issue tracker, so upstream bug reports go to symfony/symfony; we have no leverage on prioritization and should not plan around upstream fixes for our edge cases.
Support. Excellent, with a caveat: Symfony's support commitments are for Symfony, not for Laravel's integration of Symfony. If we bypass Laravel's exception handler, we are on an unsupported path as far as the Laravel ecosystem is concerned — Ignition, Telescope, Pulse, and Horizon all assume HandleExceptions is authoritative. Support for narrow utility usage is de facto community-standard; support for handler replacement is effectively self-support.
Scaling. Negligible per-request overhead when unused — ErrorHandler::call() installs and tears down a scoped handler per invocation, so cost is bounded by call frequency (avoid putting it inside hot loops). FlattenException with full arguments can be memory-heavy on deep traces; the 8.1.8 fix removing the backtrace retained by FatalError is relevant here. The component reserves memory specifically to render fatal errors after memory_limit exhaustion — real value, since Laravel's handler often cannot render under the same condition. Static pages offload error rendering entirely from PHP to nginx, which is the only approach that scales to zero PHP capacity.
Failure modes. Ranked by severity:
APP_DEBUG-gated, plus a CI grep for Debug::enable and ErrorHandler::register outside tests/ and artisan scripts.report().Ramp-up. Low for the utility APIs — an engineer can adopt ErrorHandler::call() and FlattenException with the README and a code-review checklist. Moderate for the renderers and static-page generation, which require someone to own the nginx/error-page config and the branded template. High, and not recommended, for the global-handler path: it requires understanding Laravel's bootstrap sequence, the HandleExceptions contract, Octane's worker lifecycle, and the deprecation-detection internals of DebugClassLoader. Estimate: half a day to a working FlattenException integration, one to two weeks for a defensible static-error-page pipeline including ops sign-off, and an open-ended research spike for handler replacement that I would not fund.
How can I help you explore Laravel packages today?