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.
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.
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).
Use it for any app that serves web requests and wants sensible, safe error responses:
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.
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.
On setup the component registers PHP's standard
handlers, then routes everything through its
handle() entry point:
ErrorException.
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.
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.
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
}
}
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.
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.