Error Handling
Throw an HTTPError from anywhere. The framework catches it and renders the right error page inside your module's frame.
The HTTPError Exception
Zero provides an HTTPError exception class that gives you clean control flow for error conditions. Throw it from anywhere in your module — a controller method, a model, a helper — and the framework handles the rest.
// Basic usage - just the HTTP status code
throw new HTTPError(404);
// With a detail message for debugging
throw new HTTPError(400, "Missing required parameter: name");
// In a real endpoint
public function show($id) {
$item = $this->model->find($id);
if (!$item) {
throw new HTTPError(404, "No item with ID: $id");
}
$this->data = ['item' => $item];
$this->respond($this->viewPath . 'show.php');
}
The constructor takes two arguments:
| Parameter | Type | Description |
|---|---|---|
$code |
int |
HTTP status code (400, 403, 404, 500, etc.) |
$detail |
?string |
Optional detail message. Shown in DEVMODE and included in JSON responses. |
How It Works
The HTTPError exception bubbles up to Application::run()'s try/catch block, which calls renderError() on the already-instantiated module:
$moduleInstance = new $Module();
try {
$this->checkForAttributes($Module, $endpoint);
$moduleInstance->{$endpoint}(...$args);
} catch (HTTPError $e) {
$moduleInstance->renderError($e);
} catch (\ArgumentCountError $e) {
$moduleInstance->renderError(new HTTPError(400));
}
The renderError() method on Response sets the HTTP status header, looks for a custom error view, and calls respond() to render it within the module's frame — just like a normal page.
Why HTTPError, Not \Exception
The key reason to use HTTPError instead of a generic \Exception or \Error is that the framework catches it after the module is instantiated. This means:
- The module's
framePathis already resolved — the error page renders inside the correct layout - Custom frame files (header, footer, navigation) are preserved
- Module-specific error views can be used
- The error page looks like a natural part of the site, not a raw white page
Throwing new \Exception('Not found') or new \Error(404) will bypass the framework's error handling entirely. The user will see a raw PHP error or a blank page instead of a styled error view within your module's frame.
ArgumentCountError Handling
When a URL doesn't provide enough arguments for a method's required parameters, PHP throws an ArgumentCountError. The framework catches this automatically and converts it to an HTTPError(400):
// Method requires $id
public function show($id) { ... }
// URL: /my-module/show (no $id provided)
// PHP throws ArgumentCountError
// Framework catches it → HTTPError(400) → "Bad Request"
This means you don't need to manually check whether required arguments were provided — just declare them as required parameters and the framework handles the error case.
Custom Error Views
You can create custom error views for specific HTTP status codes in your module's view/ directory. The naming convention is error{code}.php:
When renderError() is called, it checks for a matching custom view file:
// Use a custom error view if one exists in the already-resolved viewPath
$view = file_exists($v = $this->viewPath . "error" . $code . ".php")
? $v
: "<h1>$code - $message</h1><hr>";
If no custom view exists, the framework renders a simple default error page with the status code and message.
Example Custom 404 Page
<div class="error-page">
<h1>Page Not Found</h1>
<p>The page you're looking for doesn't exist or has been moved.</p>
<a href="/">Return Home</a>
</div>
Development Mode
When DEVMODE is true (the requesting IP matches DEV_SUBNET in app/config/constants.ini), the detail message from HTTPError is shown on the error page. In production, only the standard HTTP status message is displayed.
| Context | What's Shown |
|---|---|
| Production | 404 - Not Found |
| Development | 404 - Not Found: No item with ID: 99 |
| JSON response | Detail is always included in the JSON envelope |
Always include useful detail messages in your HTTPError throws — they cost nothing in production (hidden from end users) but save debugging time in development.
Common Patterns
// 404 - Resource not found
public function show($id) {
$item = $this->model->find($id);
if (!$item) {
throw new HTTPError(404, "Item not found: $id");
}
}
// 400 - Bad input
public function update($id) {
$name = $_POST['name'] ?? null;
if (empty($name)) {
throw new HTTPError(400, "Missing required field: name");
}
}
// 403 - Custom permission check
public function admin() {
if (!$this->user->isOwner()) {
throw new HTTPError(403, "Owner access required");
}
}
// 405 - Wrong HTTP method (prefer #[AllowedMethods] attribute)
public function delete($id) {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new HTTPError(405, "POST required");
}
}
// 500 - Something went wrong
public function process() {
$result = $this->externalApi->call();
if ($result === false) {
throw new HTTPError(500, "External API unreachable");
}
}
For authentication, HTTP method restrictions, and parameter validation, use PHP attributes like #[RequireLogin] and #[AllowedMethods('POST')] instead of manual checks. Reserve HTTPError throws for business logic and cases where attributes don't apply.
Supported Status Codes
The HTTPError class includes message mappings for all standard HTTP error codes from 400 through 599, including common non-standard codes like 418 (I'm a teapot), 429 (Too Many Requests), and 503 (Service Unavailable). If a code is not in the map, the message defaults to "Unspecified".