JiFramework ships with a two-class error handling system that automatically catches every PHP error, warning, notice, and uncaught exception — then logs it and renders the appropriate response.
- Class:
JiFramework\Core\Error\ErrorHandler— registers the PHP error/exception/shutdown handlers, logs everything, determines the HTTP status code - Class:
JiFramework\Core\Error\ErrorPageHandler— renders the response: HTML error page or JSON error body depending on request type - Access:
$app->errorHandler/$app->errorPageHandler
Both handlers are registered automatically during new App(). You do not write any setup code — error handling is active from the very first line after bootstrap.
$app = new App();
// From this point, ALL PHP errors and uncaught exceptions are:
// 1. Logged to storage/Logs/app.log
// 2. Rendered as an HTML page or JSON response
// 3. Responded to with the correct HTTP status code
Three handlers are registered:
set_error_handler— catches E_WARNING, E_NOTICE, E_USER_ERROR, etc. and converts them toErrorExceptionset_exception_handler— catches all uncaught exceptions including those converted from PHP errorsregister_shutdown_function— catches fatal errors (E_ERROR, E_PARSE, E_CORE_ERROR) that the error handler cannot intercept
The app_mode setting in config/jiconfig.php controls what users see when an error occurs. The log file always receives the full details regardless of mode.
Development mode (app_mode => development):
- Full exception details shown on screen: class, message, file, line, and complete stack trace
- PHP
display_errorsis set to1anderror_reportingtoE_ALL - Use this mode on your local machine only — never on a live server
Production mode (app_mode => production):
- Generic message shown: "An unexpected error occurred. Please try again later."
- Full details are still written to the log file for you to investigate
- PHP
display_errorsis set to0 - Always use this mode on live servers to avoid leaking sensitive information
// config/jiconfig.php
return [
'app_mode' => 'production', // never show errors to users
// ...
];
What each mode renders for an unhandled exception:
// Development
Error 500
Exception: PDOException
Message: SQLSTATE[HY000]: No such file or directory
File: src/Core/Database/DatabaseConnection.php (Line 47)
Stack trace:
#0 src/Core/Database/QueryBuilder.php(83): PDO->__construct(...)
#1 pages/users.php(10): QueryBuilder->__construct(...)
// Production
Error 500
An unexpected error occurred. Please try again later.
JiFramework provides a set of typed exceptions in the JiFramework\Exceptions namespace. Throwing these instead of generic exceptions allows the error handler to map them to the correct HTTP status code automatically.
RuntimeException
|-- HttpException($statusCode, $message) base for all HTTP errors
| |-- NotFoundException HTTP 404
| |-- ForbiddenException HTTP 403
| |-- UnauthorizedException HTTP 401
|-- DatabaseException HTTP 500 (DB connection/query failures)
|-- ValidationException HTTP 500 (bad validation configuration)
HttpException and its subclasses are the key ones — they carry an HTTP status code that ErrorHandler reads directly when determining the response code. All other exception types result in a 500 response.
use JiFramework\Exceptions\NotFoundException;
use JiFramework\Exceptions\ForbiddenException;
use JiFramework\Exceptions\UnauthorizedException;
use JiFramework\Exceptions\HttpException;
// 404 - resource not found
$post = $app->db->table('posts')->where('id', $id)->first();
if (!$post) {
throw new NotFoundException('Post not found');
}
// 403 - authenticated but no permission
if ($post['user_id'] !== $app->sessionManager->get('user_id')) {
throw new ForbiddenException('You cannot edit this post');
}
// 401 - not authenticated at all
if (!$app->sessionManager->get('user_id')) {
throw new UnauthorizedException();
}
// Any specific HTTP code
throw new HttpException(429, 'Too many requests');
throw new HttpException(503, 'Service temporarily unavailable');
All of the above are caught by the registered exception handler, logged, and rendered as an HTML or JSON error response with the correct HTTP status code.
$app->abort(int $statusCode, string $message = ''): void
A clean one-line shorthand for throwing an HttpException. Use this anywhere in your application code to immediately stop the request and render an error page with the given status code.
$statusCode— (int) HTTP status code to respond with$message— (string, optional) In development mode this is shown on screen. In production only the generic message is shown, but this is always written to the log.
abort() throws an HttpException which flows through the full error pipeline: it is logged, the status code is respected, and it renders HTML or JSON based on the request type.
// 404 - resource missing
$user = $app->db->table('users')->where('id', $id)->first();
if (!$user) {
$app->abort(404, 'User not found');
}
// 403 - no permission
if ($user['role'] !== 'admin') {
$app->abort(403);
}
// 401 - not logged in
if (!$app->sessionManager->get('user_id')) {
$app->abort(401, 'Please log in to continue');
}
// 503 - maintenance mode
if ($maintenanceMode) {
$app->abort(503, 'Site is under maintenance');
}
abort() vs redirect():
- Use
abort()for hard stops where the request cannot be fulfilled — missing resource, no permission, server error - Use
redirect()when you want the user to go somewhere else — login page, previous page, dashboard
// API context - abort() returns JSON automatically
// GET /api/posts/999 with Accept: application/json
$app->abort(404, 'Post not found');
// Response: HTTP 404 {"error": {"code": 404, "message": "Post not found"}}
// Browser context - abort() returns HTML error page
// GET /posts/999 (regular browser request)
$app->abort(404, 'Post not found');
// Response: HTTP 404 styled HTML error page
By default, error pages use the built-in styled HTML page. You can replace this with your own template to match your application design.
Step 1 — Set the error_template key in config/jiconfig.php:
// config/jiconfig.php
return [
'error_template' => __DIR__ . '/../views/errors/error.php',
// ...
];
Step 2 — Create your template file. Two variables are available inside it:
$errorCode— (int) The HTTP status code, e.g.404$errorMessage— (string) Already HTML-escaped. Full details in dev mode, generic message in production.
<!-- views/errors/error.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error <?= $errorCode ?></title>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<nav><!-- your nav --></nav>
<main>
<?php if ($errorCode === 404): ?>
<h1>Page Not Found</h1>
<p>Sorry, we couldn't find what you were looking for.</p>
<a href="/">Go back home</a>
<?php elseif ($errorCode === 403): ?>
<h1>Access Denied</h1>
<p>You don't have permission to view this page.</p>
<?php else: ?>
<h1>Something Went Wrong</h1>
<p>We've been notified and will look into it.</p>
<?php endif; ?>
<?php if (isset($errorMessage) && $errorCode >= 500): ?>
<!-- Only show details in development -->
<pre><?= $errorMessage ?></pre>
<?php endif; ?>
</main>
</body>
</html>
If the template file does not exist or error_template is not set, the built-in error page is used as a fallback automatically.
Note: The custom template is only used for HTML responses. JSON error responses (API/AJAX requests) always use the built-in JSON format regardless of template configuration.
$app->errorPageHandler->handle(int $errorCode, string $message = null): void
Directly render an error response for a given HTTP status code and terminate the script. This is called internally by ErrorHandler after every uncaught exception, but you can also call it directly when you want to render an error page without going through the exception pipeline (e.g. no logging needed).
If $message is null, the default status text is used ("Not Found", "Forbidden", etc.).
Auto-detects the response format:
- JSON requests (
Accept: application/json,Content-Type: application/json, orX-Requested-With: XMLHttpRequest) receive{"error": {"code": 404, "message": "..."}} - All other requests receive the HTML error page (custom template or built-in)
// Render a 404 with the default message "Page Not Found"
$app->errorPageHandler->handle(404);
// Render a 503 with a custom message
$app->errorPageHandler->handle(503, 'Scheduled maintenance until 3 AM');
// Useful in middleware-style code before App fully boots
// (e.g. in a rate limiter or access control that runs at the top of index.php)
$app->errorPageHandler->handle(429, 'Too many requests');
Prefer $app->abort() in most cases — it goes through the full pipeline (logging included). Use handle() directly only when you explicitly do not want the exception to be logged.
The @ error-suppression operator works correctly with JiFramework. Prefixing a function call with @ tells PHP to suppress any errors it generates, and the framework respects this by checking error_reporting() before converting a PHP error into an ErrorException.
// Without @ — a failed file read triggers E_WARNING which becomes an ErrorException
$content = file_get_contents('/path/to/maybe-missing-file.txt');
// Throws ErrorException if file not found
// With @ — the warning is suppressed, returns false gracefully
$content = @file_get_contents('/path/to/maybe-missing-file.txt');
if ($content === false) {
// handle gracefully — no exception thrown
$content = 'default content';
}
// Common use: checking remote URLs without throwing on network failure
$response = @file_get_contents('https://external-api.com/data');
if ($response === false) {
$app->logger->warning('External API unreachable');
$response = $app->cache->get('api_last_known', '');
}
Note: Use @ sparingly. It only suppresses errors — it does not fix them. Always check the return value after using it.
A realistic page handler demonstrating all error handling patterns together:
<?php
require __DIR__ . '/vendor/autoload.php';
use JiFramework\Core\App\App;
use JiFramework\Exceptions\NotFoundException;
use JiFramework\Exceptions\ForbiddenException;
use JiFramework\Exceptions\UnauthorizedException;
$app = new App();
// ErrorHandler is now registered — all errors/exceptions are caught from here
$sm = $app->sessionManager;
// 1. Auth guard — throw if not logged in
if (!$sm->get('user_id')) {
throw new UnauthorizedException();
// or: $app->abort(401);
}
$postId = (int)($_GET['id'] ?? 0);
// 2. Find resource — throw 404 if missing
$post = $app->db->table('posts')->where('id', $postId)->first();
if (!$post) {
throw new NotFoundException('Post #' . $postId . ' does not exist');
// or: $app->abort(404, 'Post not found');
}
// 3. Ownership check — throw 403 if not owner
if ($post['user_id'] !== $sm->get('user_id')) {
throw new ForbiddenException('You do not own this post');
// or: $app->abort(403);
}
// 4. @ suppression for optional external data
$meta = @file_get_contents('https://meta-service/posts/' . $postId);
if ($meta === false) {
$meta = null; // graceful fallback, no exception
}
// 5. Everything OK — render the page
include 'views/posts/edit.php';
If any exception escapes from the page (database error, unexpected null, etc.), ErrorHandler catches it automatically, logs the full details, and renders the appropriate error page without any extra code from you.