Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Error Handler Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

You almost certainly already have this package: laravel/framework requires symfony/error-handler transitively (Laravel's HandleExceptions bootstrap uses FatalError from it to convert shutdown-time fatals into exceptions). Confirm before adding anything:

composer show symfony/error-handler   # check the version Laravel pinned (^7.2 on L11/L12)

Don't composer require it to "install" it — you'd only do that to force a version, and v8 requires PHP >= 8.4, which will fight your framework constraint.

Where to look first, in order of day-to-day value:

  1. Symfony\Component\ErrorHandler\ErrorHandler::call() — the only API you should reach for inside a Laravel app.
  2. Symfony\Component\ErrorHandler\FlattenException — portable, array-backed exception DTO.
  3. Symfony\Component\ErrorHandler\Debug / DebugClassLoader — for standalone scripts and upgrade tooling, not for bootstrap/app.php.
  4. HtmlErrorRenderer / CliErrorRenderer — only relevant outside Laravel's own renderer.

First use case. Laravel's HandleExceptions::handleError() guards with error_reporting() & $level, so anything behind @ is silently dropped and never reaches Handler::report(). That is exactly the gap this component fills:

use Symfony\Component\ErrorHandler\ErrorHandler;

// Inside a queued job / service class
$config = ErrorHandler::call(function () use ($path) {
    // A failed file_get_contents() now throws ErrorException,
    // even if the underlying lib used @file_get_contents().
    return json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
});

Now the job fails, Laravel's retry/backoff and failed() hooks fire, and the incident shows up in your logs instead of producing a null that blows up three layers later.

Implementation Patterns

Fail-fast scopes around critical I/O. Wrap the smallest possible closure — one file read, one fopen, one legacy SDK call — not a whole method:

public function handle(Invoice $invoice): void
{
    $raw = ErrorHandler::call(fn () => file_get_contents($invoice->storagePath()));

    $this->gateway->charge(ErrorHandler::call(
        fn () => $this->client->post('/charges', ['json' => $invoice->payload()])
    ));
}

ErrorHandler::call() returns the closure's return value, is re-entrant, and restores the previous handler in a finally. It is not a try/catch replacement — it only converts PHP errors/notices/warnings into ErrorException; real exceptions propagate untouched.

Standalone scripts, cron jobs and microservices. Where Laravel isn't booted (a php scripts/backfill.php, a worker entrypoint, a package's own CLI), Debug::enable() is the fastest sane setup:

require __DIR__.'/vendor/autoload.php';

Symfony\Component\ErrorHandler\Debug::enable(); // error handler + exception handler + DebugClassLoader

If you want the error-to-exception behaviour without taking over the exception handler / page rendering, call ErrorHandler::register() alone instead of Debug::enable().

Route PHP notices into your Laravel log. In a standalone context, or if you register the handler manually, give it a PSR-3 sink — Laravel's logger qualifies:

ErrorHandler::setDefaultLogger(Log::getLogger()); // Illuminate\Log\Logger::getLogger() -> PSR-3

Beware recursion: a logger that itself raises warnings has historically caused loops (patched upstream). Never point this at a channel that can throw during formatting.

DebugClassLoader as an upgrade linter. This is the most underrated use. Enable it only in a throwaway run — a PHPUnit bootstrap, or a dedicated script — and run your test suite; it inspects every class as Composer loads it and emits deprecations for missing/incompatible return types, @method annotations, generics mismatches, etc. It's how you find "this will break on PHP 8.4 / Symfony 8" before the upgrade, not after.

// tests/bootstrap-debug-classloader.php — dev only, never in prod
Symfony\Component\ErrorHandler\DebugClassLoader::enable();

v8.1 also added namespace remapping so you can relax the "same vendor" constraint when checking third-party code.

Portable error payloads with FlattenException. If you're writing a package or a service that must shape errors identically inside and outside Laravel, FlattenException::createFromThrowable($e) gives you a serialisable DTO with getStatusCode(), getStatusText(), getHeaders() and toArray() — the same shape Symfony's renderers and Messenger use to ship exceptions across process boundaries. Prefer this to inventing your own error envelope.

Custom / static error pages for non-Laravel surfaces. For the Nginx error_page fallback, a maintenance CDN page, or a non-Laravel service, HtmlErrorRenderer::setTemplate('/path/to/error.html.php') swaps the generic non-debug template, and recent 7.3+ releases ship a command to dump static error pages for exactly this. Note this is a static setting with global effect — see gotchas.

Gotchas and Tips

  • Never call Debug::enable() inside a Laravel app. It installs both an error handler and an exception handler, so you lose Laravel's Handler::report(), your log channels, Ignition/Whoops, and JSON API responses in one line — uncaught exceptions start rendering Symfony HTML. Use ErrorHandler::call() for scoped behaviour and leave Laravel's pipeline intact.
  • @-suppression is a blast radius, not a feature. Because ErrorHandler::call() throws even on silenced errors, a wide closure drags Guzzle, the AWS SDK, Flysystem, and PHPUnit internals into "throw on any notice" territory. Keep scopes one-operation wide.
  • Deprecations no longer throw by default. Since 7.3.1 the component silences E_DEPRECATED/E_USER_DEPRECATED; don't build a deprecation gate on ErrorHandler::call(). Use PHPUnit's deprecation handling or a set_error_handler of your own.
  • Long-running workers (Octane, Horizon). Handlers registered per-request persist across requests in a resident worker; repeated register() calls stack up. There was an upstream fix for a leaking exception handler when not replacing the error handler — pin a recent patch release. ErrorHandler::call() is scoped and safe here; manual register() is not.
  • Static global state. HtmlErrorRenderer::setTemplate() and ErrorHandler::setDefaultLogger() are process-wide and leak between tests. Set them in a bootstrap, not in a test case.
  • If you do register() manually, keep the returned instance and restore with restore_error_handler() / restore_exception_handler() in a finally. Otherwise you silently disable Laravel's error handling for the rest of the request.
  • DebugClassLoader is slow and noisy. It reflects on every autoloaded class, including vendor. Scope it to a CI job, filter with its ignore list, and never enable it in a hot path or production.
  • Debugging a "nothing happened" case. Check error_get_last() and whether an earlier set_error_handler already swallowed the error; remember ErrorHandler::call() will not fire for errors raised outside the closure.
  • Extension points worth knowing: implement ErrorRendererInterface if you need a bespoke format; ExceptionHandler + HtmlErrorRenderer if you want Symfony's full debug page in a non-Laravel app; ErrorHandler scopes/throwAt config if you need to tune which levels throw.
  • Version discipline. v6.4 is LTS; v7.x is current for Laravel 11/12; v8.x demands PHP 8.4+. Pin deliberately and read the patch notes — this package ships behaviour-changing bugfixes (deprecation silencing, fatal-error backtraces, __serialize replacing __sleep) that can shift what your error path does between patch releases.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo