ForgeErrorHandler

A component that handles errors in your app: it turns uncaught exceptions and PHP errors into clean responses — a rich debug page while you develop, a friendly page (or JSON) in production — and logs what went wrong along the way.

Overview

This component plugs into the error-handling contract used by your web app. It implements ErrorHandlerInterface — the contract that turns a Throwable and the current request into a Response — so you don't have to write that plumbing yourself. You still decide how your app behaves on the other side of an error.

What it gives you

  • Catches exceptions and PHP errors
  • Debug page with code snippets and trace
  • Production page that hides internals
  • JSON responses for API routes
  • Logs to a PSR-3 logger or a file
  • Masks passwords, tokens, and secrets
  • CLI error handling for commands

Plugs into ForgeRouter: ForgeErrorHandler implements Modules\ForgeRouter\Contracts\ErrorHandlerInterface, so it works with the web layer rather than the Kernel alone. It ships core (core: true, loaded at order: 2).

When to Reach for It

Use it for any app that serves web requests and wants sensible, safe error responses:

  • You want detailed errors in development but a clean, leak-free page in production.
  • Your API should return JSON errors instead of an HTML page.
  • You want every failure logged with context (request, trace, timing) without wiring it by hand.

If you're building a pure CLI/worker tool with no web layer, this component has less to offer — the Kernel's own CLI error handling may be enough.

Installation

A core capability, so usually present. To add it to a project:

php forge.php package:install-module --module=ForgeErrorHandler

It depends on the router's error contract, so it's expected to be used alongside the web layer.

How It Works

On setup the component registers PHP's standard handlers, then routes everything through its handle() entry point:

  • Error handler — PHP warnings and notices are converted to an ErrorException.
  • Exception handler — uncaught exceptions produce a response (or CLI error) and exit.
  • Shutdown handler — fatal errors (memory, parse, compile) are caught on shutdown.

Every handled error is logged first, then turned into a response whose shape depends on your APP_DEBUG setting and whether the request is an API call.

Debug vs Production

The behavior flips on your APP_DEBUG environment setting:

Mode HTML (browser) API (JSON)
Debug (APP_DEBUG=true) Rich page: type, message, code snippets, filtered trace, request/session data. Pretty JSON with the full error and trace.
Production (APP_DEBUG=false) Friendly 500 page; internals never exposed. Minimal JSON: a 500 "An error occurred" message.

PII and secrets are masked in both modes — keys like password, token, secret, authorization, and cookie are shown as masked values.

API Requests

A request is treated as an API call when it signals JSON — the Accept or Content-Type header contains application/json, or the URI starts with /api. API calls always get JSON errors instead of an HTML page.

{
    "error": {
        "message": "An error occurred. Please try again later.",
        "type": "InternalServerError",
        "code": 500
    }
}

Error Logging

Every handled error is logged with rich context. If a PSR-3-compatible logger is available it's used; otherwise the component writes to storage/logs/errors.log.

Each entry captures the fingerprint, a request id, the exception class and message, file and line, the trace, peak memory, duration, source (client IP, method, URI — or the CLI command), user agent, and session/query/post data:

[2026-08-27 12:00:00] 9f1c2a3b4d5e6f7 [ab12cd34] RuntimeException – src/App/Controller.php:42 | #0 ... | Something broke

Logging is rate-limited per error fingerprint, so a repeating failure doesn't flood the log.

Writing Your Own

This is a component, not a cage. If your app wants its own behavior — different pages, custom logging, a different response format — implement the same contract yourself:

use Modules\ForgeRouter\Contracts\ErrorHandlerInterface;
use Modules\ForgeRouter\Http\Request;
use Modules\ForgeRouter\Http\Response;
use Throwable;

class MyErrorHandler implements ErrorHandlerInterface
{
    public function handle(Throwable $e, Request $request): Response
    {
        // Log, format, and return whatever your app needs
        return new Response('Something went wrong', 500);
    }
}

Anything implementing the interface can be bound in its place — your handler, or another capability.