Error Handling
When a handler throws, Fast catches it so a single broken request never takes down the worker. What the visitor sees depends on how you have set things up: a bare 500 by default, a detailed developer page while you are debugging, or a custom response you define with on_error().
The default
Out of the box, an uncaught exception becomes a plain 500 Internal Server Error. The failure is recorded through the logger first, so you always have a trail, and nothing leaks to the client but the status.
A typed HttpException is different: it renders itself into the status you asked for, so throw new HttpException(404) is a real 404, not a caught failure. Those never reach the error handler described here. The same goes for a 404 or 405 from routing, which are ordinary responses rather than thrown errors.
The developer error page
While you are building, a bare 500 hides the very thing you need to see. Turn on debug mode and Fast replaces it with a full error page: the exception class and message, the file and line, a snippet of the offending source, and the trace. Every value is escaped, and the page renders only the exception, never your environment, headers, or request data.
Switch it on when you start the dev server:
php server.php --watch --debug
Or set the environment variable, which is what --debug does under the hood:
APP_DEBUG=1
Leave it off in production. The page is meant for your eyes, and a bare 500 is the right thing for a real visitor to see.
A custom error response with on_error()
For full control, register a renderer with on_error(). It receives the throwable and the current request, and returns whatever a handler may return: a Response, a string, or an array to encode as JSON.
use Fast\Http\Request;
use Fast\Http\Response;
on_error(function (\Throwable $e, Request $request): Response {
if (str_starts_with($request->path, '/api/')) {
return json(['error' => 'Something went wrong'])->withStatus(500);
}
return html(render('errors.500'), 500);
});
An on_error() renderer wins over the developer page, so once you register one it owns every uncaught failure. It fires only for genuine errors, never for an HttpException or a 404. If your renderer itself throws, Fast logs that too and falls back to a safe built-in 500, so a bug in your error page can never spiral.
The precedence, from first match to last resort:
- your
on_error()renderer, if you registered one; - the developer error page, if debug mode is on;
- a bare
500 Internal Server Error.
A custom 404 with not_found()
An uncaught error and a missing route are different things, so they have different hooks. When no route matches, Fast returns a plain 404 Not Found. To style that page, register a handler with not_found():
use Fast\Http\Request;
not_found(function (Request $request) {
if (str_starts_with($request->path, '/api/')) {
return ['error' => 'Not found'];
}
return html(render('errors.404'));
});
The handler may return anything a route handler can: a Response, a string, or an array to encode as JSON. Whatever you return is sent with a 404 status, so you do not have to set it yourself (a Response you return keeps its own status). It fires only for a genuine no-route-match; a 405 Method Not Allowed is a different case and does not route here. If your handler throws, Fast logs it and falls back to the bare 404, so a bug in your not-found page can never spiral.
Next up is logging, which is where those caught failures are recorded.