northrook / error-handler
Capture and handle errors, build structured reports, and render consistent output
Requires
- php: >=8.4
- northrook/ansi-formatter: dev-main
- northrook/core-contracts: dev-main
- northrook/hasher: dev-main
- northrook/inline-styles: dev-main
- northrook/php-debug: dev-main
Requires (Dev)
- northrook/php-cs: dev-main
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-07-16 18:05:23 UTC
README
Capture, buffer, aggregate, and render errors. Every event is recorded; emit channels (PSR log, render, terminate) apply filters and priority separately.
Requires PHP 8.4+.
Install
composer require northrook/error-handler
Quick start
use Northrook\ErrorHandler;
use Northrook\ErrorHandler\ErrorHandlerConfig;
use Northrook\ErrorHandler\ErrorLevelMask;
use Psr\Log\LoggerInterface;
$handler = ErrorHandler::register(
logger: $psrLogger, // optional; defaults to NullLogger
throwAt: ErrorLevelMask::Standard, // optional; see Throw mask
config: new ErrorHandlerConfig(debug: true),
);
// Uncaught exceptions and fatals are handled automatically once installed.
register() builds the singleton and, by default, installs global error / exception / shutdown handlers. Pass install: false to construct without wiring globals, then call $handler->install() when ready.
Design
| Principle | Meaning |
|---|---|
| Buffer ≠ log | The buffer is the full forensic record (including @-silenced). Logging is filtered. |
| Report ≠ handle | report() builds diagnostics without dying; handle() renders and exits. |
| Global ≠ scoped | Global handlers enforce throwAt; box() probes without throwing. |
| Priority ≠ PHP severity | EventPriority is consequence-based (Noise / Action / Critical), not E_*. |
Pipeline (current):
PHP error / throwable / box probe
↓
ErrorBuffer (always)
↓
Fingerprint → Aggregator (count + highest priority)
↓
Route: Critical → log now; else queue until shutdown flush
↓
handle() also renders + exit(1)
Registration & lifecycle
ErrorHandler::register()
ErrorHandler::register(
null|LoggerInterface $logger = null,
null|ErrorRendererInterface $renderer = null,
null|int|ErrorLevelMask $throwAt = null,
bool $install = true,
null|ErrorHandlerConfig $config = null,
): ErrorHandler
| Argument | Default | Role |
|---|---|---|
$logger | NullLogger | PSR-3 sink for aggregate emit |
$renderer | ErrorRenderer (CLI/HTML/JSON) | Fatal / handle() output |
$throwAt | ErrorLevelMask::Standard | Which PHP E_* types become exceptions |
$install | true | Wire global handlers immediately |
$config | new ErrorHandlerConfig() | Debug flag, render strategy, palette |
Install / uninstall / reset
$handler->install(); // set_error_handler, set_exception_handler, register_shutdown_function
$handler->uninstall(); // restore previous handlers
$handler->reset(); // uninstall (if installed), clear buffer + aggregator, drop singleton
Shutdown always flushes pending aggregates to the logger first, then handles fatal last-errors.
Configuration
use Northrook\Contracts\RenderStrategy;
use Northrook\ErrorHandler\ErrorHandlerConfig;
new ErrorHandlerConfig(
debug: true, // default: AppEnv::isDebug()
renderStrategy: RenderStrategy::AUTO, // honored by ReportFactory / dumps
colorPalette: null, // optional ColorPalette override
);
debug— whentrue,context['dumps']are turned into VarDump bags for CLI/HTML. Whenfalse, dumps are ignored.renderStrategy— passed into dump rendering (AUTOpicks CLI vs HTML by SAPI).
Throw mask (ErrorLevelMask)
Controls which PHP error types the global handler promotes to ErrorException. Independent of priority / logging.
use Northrook\ErrorHandler\ErrorLevelMask;
ErrorHandler::register(throwAt: ErrorLevelMask::Standard);
ErrorHandler::register(throwAt: E_USER_WARNING | E_USER_ERROR); // raw int also fine
| Case | Mask |
|---|---|
None | 0 — never throw from PHP errors |
Fatal | Fatals + recoverable |
Standard | E_ALL minus deprecations (default) |
WarningsAndAbove | Excludes notices and deprecations |
Strict | E_ALL including deprecations |
Silenced errors (error_reporting masks the type, or @) are always buffered and aggregated as Noise; they never throw.
Scoped probes — box()
Failsafe: catch PHP errors inside a callback, keep going. Never throws from the scoped handler.
$result = $handler->box(
fn () => $cache->warm($key),
// optional: EventPriority::Action,
);
if ($handler->lastBoxError() !== null) {
// inspect RuntimeError; process continues
}
| Behaviour | Detail |
|---|---|
| Default priority | Noise |
| Optional 2nd arg | EventPriority — elevates the aggregate for that scope |
| Buffer | Errors recorded on the shared ErrorBuffer |
| Aggregate | Each distinct fingerprint is hit; repeats bump count |
| Return | Callback return value (or null if the callback returns nothing useful) |
lastBoxError() is the last RuntimeError from the most recent box(); lastError() is the last entry in the global buffer.
Reports — report() vs handle()
report(Throwable $e, array $context = []): ErrorReport
Builds a structured ErrorReport (reference, snapshot, stack, previous chain, context, dumps, PHP errors). Does not exit.
Default priority: Action. Override with reserved context key:
$report = $handler->report($e, [
'priority' => EventPriority::Critical, // or 'Critical' / 'critical'
'orderId' => $orderId,
'dumps' => [
'cart' => $cart,
'user' => $user,
],
]);
echo $handler->renderer->json->render($report); // or cli / html
Reserved keys (stripped from stored context):
| Key | Effect |
|---|---|
priority | EventPriority (or case name string) — emit routing |
dumps | Labeled values → debug dump bags when config.debug is on |
handle(Throwable $e): never
Forces Critical, aggregates, logs immediately, sets HTTP 500 (when not CLI and headers not sent), renders via the composite renderer, then exit(1).
Uncaught exceptions go through handle() (except under PHPUnit, where they are rethrown).
Priority & aggregation
EventPriority
| Band | Meaning | Emit |
|---|---|---|
Noise | Forensic / hygiene | Batched at shutdown |
Action | Should be noticed | Batched; appears in actionables() |
Critical | Cannot usefully continue | Immediate log |
Defaults by path:
| Path | Priority |
|---|---|
Untagged box() | Noise |
Untagged report() | Action |
handle() / fatal shutdown | Critical |
| Global non-throwing / silenced | Noise |
Repeats of the same fingerprint elevate to the highest priority seen and bump count.
Fingerprint
Stable xxh32 of class|phpType + file + line + message — used for aggregation. Separate from the per-instance report reference (error-{xxh32}).
Standing list & flush
$handler->actionables(); // Action+ entries this request
$handler->aggregator()->all(); // every fingerprint
$handler->flushPendingAggregates(); // emit queued Noise/Action now (also called on shutdown)
Each emit is one PSR log line per fingerprint:
| Log level | Priority |
|---|---|
notice | Noise |
error | Action |
critical | Critical |
Context includes fingerprint, priority, count, class, reference (when a report exists), firstSeen, lastSeen.
Buffer inspection
$buffer = $handler->errors(); // ErrorBufferInterface (shared)
$buffer->all();
$buffer->last();
$mark = $buffer->mark();
$since = $buffer->since($mark);
Everything that hits the global or scoped handlers is recorded here, including silenced notices.
Renderers
ErrorRenderer picks CLI vs HTML by SAPI. JSON is available explicitly.
| Backend | Output |
|---|---|
CliRenderer | ANSI summary, stack, previous chain, dump CLI blobs |
HtmlRenderer | Error page with SourceView panes + HTML dump sections |
JsonRenderer | $report->jsonString() |
Source highlighting and var dumps come from northrook/php-debug — this package wires them in; it does not reimplement them.
Composite construction (optional overrides):
use Northrook\ErrorHandler\ErrorRenderer;
use Northrook\ErrorHandler\Renderer\CliRenderer;
use Northrook\ErrorHandler\Renderer\HtmlRenderer;
use Northrook\ErrorHandler\Renderer\JsonRenderer;
$renderer = new ErrorRenderer(
cli: new CliRenderer(),
html: new HtmlRenderer(new ErrorHandlerConfig(debug: true)),
json: new JsonRenderer(),
);
If a backend renderer itself throws, a dependency-light disaster fallback is printed and the failure is logged at critical.
Typed exception meta
ReportFactory attaches snapshot meta for known contract exceptions (e.g. filesystem path, curl URL). Extend buildMeta() as more types land in contracts.
Demos
From the package root:
php index.php # list demos
php index.php exception # uncaught → handle() view
php index.php box # failsafe probe
php index.php dumps # context dumps in CLI/HTML
php index.php aggregate # Noise counts + Action standing list + flush
# Browser
php -S localhost:8080 index.php
# open /?demo=exception
| Key | Scenario |
|---|---|
exception | Uncaught exception → full error view |
nested | Previous-exception chain |
warning | PHP warning promoted by throwAt |
box | Boxed probe; process continues |
silenced | Silenced notice buffered, then a real failure |
json | report() + JSON only (no exit) |
meta | Typed exception meta on the snapshot |
dumps | dumps => […] in report context |
aggregate | Repeated box Noise + Action report; standing list + batched flush |
Tests & static analysis
composer test
composer phpstan
Not yet implemented
Deferred (see PLAN.md): SilencePolicy / screamAt, ReportVerbosity Safe redaction, RuntimeMode presets, signature rules, SQLite/file persistence, WTHH disaster file, notification delivery.