Skip to content

Global error handler

QuietGuard\Monitor\ErrorHandler registers PHP's global handlers so a plain-PHP host with no framework exception pipeline still reports uncaught errors. Frameworks (Laravel, Symfony) have their own hooks and should not call this.

Registering

ErrorHandler::register() is a single static call that takes a configured Reporter:

php
use QuietGuard\Monitor\ErrorHandler;
use QuietGuard\Monitor\Reporter;

/** @var Reporter $reporter */
ErrorHandler::register($reporter);

Call it once, as early as possible in your bootstrap, after the Reporter is built.

What it registers

register() wires three global hooks:

  1. set_exception_handler, any uncaught Throwable is sent via $reporter->reportException($e). The handler chains: if another global exception handler was registered before ErrorHandler::register(), it still runs after the exception has been reported (the monitor reports first, then delegates). When no previous handler exists, the exception (message, file:line and stack trace) is written to error_log, matching what PHP's default behavior would have recorded: installing monitoring never removes the host's local crash record.
  2. set_error_handler, a raised PHP error is wrapped in an ErrorException and reported, provided its severity is at or above the floor described below. It first checks error_reporting() & $severity, so values suppressed with @ or below the current error_reporting level are skipped. It then returns false so PHP's normal error handling still runs.
  3. register_shutdown_function, on shutdown, error_get_last() is inspected and, if it is a fatal type (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR), an ErrorException is reported. This catches fatals that the error handler cannot.

Every path runs through the Reporter, which never throws, so registering these handlers cannot break the host.

The severity floor, and the cap that goes with it

register() takes two optional arguments:

php
public static function register(
    Reporter $reporter,
    ?int $severities = null,   // null takes E_USER_ERROR | E_RECOVERABLE_ERROR
    int $maxPerRequest = 20,
): void

By default only E_USER_ERROR and E_RECOVERABLE_ERROR are reported. Every severity used to be forwarded, one synchronous POST each, with no floor, no de-duplication and no cap: a single E_WARNING inside a loop over five hundred rows was five hundred blocking HTTP calls in one page load, which reads to a visitor as an outage and burns a free plan's monthly events in a day.

The true fatals are absent from the default on purpose. PHP calls no user error handler for E_ERROR, E_PARSE, E_CORE_ERROR or E_COMPILE_ERROR, and the shutdown hook above is what catches those already. What the default keeps is the pair a host actually raises and means: a userland fatal, and a type error PHP let through.

Two guards apply whatever the mask:

  • one report per site per request: the same error raised on the same file:line a hundred times is one fact, and the other ninety-nine POSTs are the host application's latency, not yours to spend;
  • a cap of $maxPerRequest, 20 by default, because a mask cannot see a loop: fifty distinct lines pass any floor and are still fifty requests.

Pass the mask explicitly to widen it, knowing what it costs:

php
// The old behaviour: every severity error_reporting lets through
ErrorHandler::register($reporter, E_ALL);

// Warnings too, but at most five reports in one request
ErrorHandler::register($reporter, E_USER_ERROR | E_RECOVERABLE_ERROR | E_WARNING, 5);

A minimal plain-PHP bootstrap

php
use QuietGuard\Monitor\Config;
use QuietGuard\Monitor\ErrorHandler;
use QuietGuard\Monitor\Reporter;
use QuietGuard\Monitor\Http\CurlHttpClient;
use QuietGuard\Monitor\Payload\ExceptionPayloadBuilder;
use QuietGuard\Monitor\Support\Scrubber;

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

$config = new Config(url: 'https://monitor.example.com', key: 'project-token');

$reporter = new Reporter(
    config: $config,
    http: new CurlHttpClient(),
    scrubber: new Scrubber(['password', 'authorization']),
    builder: new ExceptionPayloadBuilder($config->traceLimit, $config->release),
);

ErrorHandler::register($reporter);

// From here on, uncaught exceptions, PHP errors and fatal shutdown errors
// are forwarded to Quiet Guard automatically.
The Laravel SDK and the Symfony bundle do not use ErrorHandler: each hooks its framework's own exception pipeline. The WordPress plugin does use it, WordPress having no exception pipeline of its own, and takes the default floor. Reach for ErrorHandler in a host that has no exception pipeline of its own.

You are reading the PHP Core v1.0 documentation.