I built a custom error handling system, which I find extremely handy, so I’m sharing all of the code below. Feel free to use it however you’d like.

Over the past year, I’ve been slowly building my side project Crafd.com on weekends. As the sole developer, I’ve handled all of the design, front-end and back-end work.
I built Crafd using vanilla PHP and JavaScript, without any frameworks or libraries. It’s been enjoyable to pare back the bloat of modern web development and focus on raw code again.
Testing has never been my forte. That said, I recognize the value of having some sort of validation to quickly catch bugs after deploying changes. Rather than use an external error handling service, I decided to roll my own error handling system as a learning exercise.
I centralized all MySQL, PHP and JavaScript errors into one UI. Below I will share the code I used to create this error handling system.
Preview
Here’s a quick preview of the page in action:

Code
Here is a recap of the code that is used to run this page:
HTML
<pre class="wp-block-syntaxhighlighter-code"><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>Error Dashboard</title>
<link rel="stylesheet" type="text/css" charset="utf-8" media="screen, projection" href="/css/crafd-admin.css" />
</head>
<body>
<div class="backdrop"></div>
<div class="admin-container">
<!-- LEFT COLUMN -->
<div class="admin-left">
<header>
<a href="" class="menu">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12H21" stroke="#3E3E3E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6H21" stroke="#3E3E3E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 18H21" stroke="#3E3E3E" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
<h2>Error Logs</h2>
</header>
<div class="log-files">
<?php foreach ($errors as $key => $error) { ?>
<div class="log-file" id="log-<?php echo safe($key); ?>" data-key="<?php echo safe($key); ?>">
<span class="log-file-name"><?php echo safe($error['name']); ?></span>
<span class="log-file-size"><?php echo safe($error['size']); ?></span>
<a href="/admin/errors/delete/<?php echo str_replace('.log', '', safe($error['name'])); ?>" class="log-file-delete">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</a>
</div>
<?php } ?>
</div>
</div>
<!-- TABLE -->
<div class="admin-right">
<div class="top-bar">
<div class="top-bar-filters">
<a href="" class="filter filter-mysql show" data-type="mysql">
<svg height="15" viewBox="0 0 14 15" width="14" xmlns="http://www.w3.org/2000/svg"><g style="stroke:#9E9E9E;stroke-width:1.333333;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)"><ellipse cx="6" cy="2" rx="6" ry="2"/><path d="m12 6.66666667c0 1.10666666-2.66666667 2-6 2s-6-.89333334-6-2"/><path d="m0 2v9.3333333c0 1.1066667 2.66666667 2 6 2s6-.8933333 6-2v-9.3333333"/></g></svg>
MySQL
</a>
<a href="" class="filter filter-php show" data-type="php">
<svg height="10" viewBox="0 0 15 10" width="15" xmlns="http://www.w3.org/2000/svg"><g style="stroke:#9E9E9E;stroke-width:1.333333;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)"><path d="m9.33333333 8 3.99999997-4-3.99999997-4"/><path d="m4 0-4 4 4 4"/></g></svg>
PHP
</a>
<a href="" class="filter filter-s3 show" data-type="js">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg>
S3
</a>
<a href="" class="filter filter-js show" data-type="js">
<svg height="11" viewBox="0 0 14 11" width="14" xmlns="http://www.w3.org/2000/svg"><text fill="#9E9E9E" fill-rule="evenodd" font-family="SFProDisplay-Semibold, SF Pro Display" font-size="12" font-weight="500" letter-spacing=".0873409128"><tspan x="0" y="10">JS</tspan></text></svg>
JavaScript
</a>
</div>
<div class="top-bar-search">
</div>
</div>
<!-- TABLE -->
<table class="errors" cellpadding="0" cellspacing="0" border="0">
<thead>
<tr>
<th></th>
<th>Time</th>
<th>Message</th>
<th>File</th>
<th>Line</th>
<th>User</th>
<th>URL</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<!-- JAVASCRIPT -->
<script>
var errors = <?php echo json_encode(safe($errors)); ?>;
</script>
<a href="/js/crafd-utility.js">/js/crafd-utility.js</a>
<a href="heading%20">/js/admin/error.js/script?php%20require_once(PUBPATH.'templates/error-row.php');%20?/body/html/pre!--%20/wp:syntaxhighlighter/code%20--!--%20wp:heading%20</a></pre>
PHP (/api/error/)
The following code processed JS errors from window.onerror:
<?php if (!defined('COREPATH')) exit('No direct script access allowed');
// Prep post variables
$type = 'js';
$message = (isset($_POST['message'])) ? $_POST['message'] : '';
$file = (isset($_POST['file'])) ? $_POST['file'] : '';
$line = (isset($_POST['line'])) ? $_POST['line'] : '';
$url = (isset($_POST['url'])) ? $_POST['url'] : '';
// Pass to error handler
return error($type, $message, $file, $line, $url);
/* CLOSE DB & SESSIONS ------------------------------------------- */
closeDbAndSessions();
PHP (Controller)
<?php if (!defined('APPPATH')) exit('No direct script access allowed');
/* ADMIN CHECK ------------------------------------------- */
// Code here to prevent unauthorized access
/* CHECK IF DELETE ------------------------------------------- */
if (isset($glob['route'][2]) && $glob['route'][2] == 'delete') {
$file = $glob['route'][3] . '.log';
$path = APPPATH . 'errors/';
if (file_exists($path.$file)) {
unlink($path.$file);
}
return redirect('admin/errors');
}
/* VARS ------------------------------------------- */
$errors = [];
/* LOAD FILESYSTEM FUNCTION ------------------------------------------- */
require_once(COREPATH.'libraries/Filesystem.php');
/* PREP LOG FILES ------------------------------------------- */
$path = APPPATH.'errors/';
$csv_files = filesystem_directory_map($path);
/* ITERATE OVER LOG FILES ------------------------------------------- */
for ($i = 0; $i < count($csv_files); $i++) {
// Get file name
$file_name = $csv_files[$i];
$log_file = $path . $file_name;
$errors[$i]['name'] = $file_name;
// Get file contents
$file_contents = file_get_contents($log_file);
// Get file size
$file_size = filesystem_filesize($log_file);
$errors[$i]['size'] = $file_size;
// Loop through each line
$lines = explode("\n", $file_contents);
// Loop through each line
$errors[$i]['lines'] = [];
foreach ($lines as &$line) {
// If line is empty, skip it
if (empty($line)) {
continue;
}
// Repopulate line breaks
$line = str_replace('~~', "<br>", $line);
$parts = explode('|', $line);
// If parts is not 7 parts, skip it
if (count($parts) != 7) {
continue;
}
$errors[$i]['lines'][] = [
'type' => $parts[0],
'time' => $parts[1],
'message' => $parts[2],
'file' => $parts[3],
'line' => $parts[4],
'username' => $parts[5],
'url' => $parts[6]
];
}
}
// Sort by date
asort($errors);
/* LOAD VIEW ------------------------------------------- */
if (isset($glob['view'])) {
require_once($glob['view']);
}
/* CLOSE DB & SESSIONS ------------------------------------------- */
closeDbAndSessions();
PHP (Filesystem.php)
<?php if (!defined('COREPATH')) exit('No direct script access allowed');
function filesystem_directory_map($sourceDir, $directoryDepth = 0) {
try {
if (!is_dir($sourceDir)) {
return false;
}
$fp = opendir($sourceDir);
$fileData = [];
$newDepth = $directoryDepth - 1;
$sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
while (false !== ($file = readdir($fp))) {
// Remove '.', '..'
if ($file === '.' || $file === '..') {
continue;
}
if (is_dir($sourceDir . $file)) {
$file .= DIRECTORY_SEPARATOR;
}
if (($directoryDepth < 1 || $newDepth > 0) && is_dir($sourceDir . $file)) {
$fileData[$file] = directory_map($sourceDir . $file, $newDepth);
} else {
$fileData[] = $file;
}
}
closedir($fp);
return $fileData;
} catch (Throwable $e) {
return [];
}
}
function filesystem_filesize($log_file) {
$size_in_bytes = filesize($log_file);
$size_in_kb = $size_in_bytes / 1024;
$size_in_megabytes = $size_in_bytes / 1024 / 1024;
// If less than 1kb, return in bytes
if ($size_in_kb < 1) {
return $size_in_bytes . ' B';
}
// If less than 1mb, return in kb
if ($size_in_megabytes < 1) {
return round($size_in_kb, 2) . ' KB';
}
// Else return in mb
return round($size_in_megabytes, 2) . ' MB';
}
PHP (Bootstrap.php)
This code runs in my bootstrap file to kickstart the app on every page.
/* ERROR TRACKING ------------------------------------------- */
require_once(COREPATH.'libraries/Error.php');
// Set default error function for PHP
set_error_handler('php_error_handler');
// Catch fatal PHP errors
register_shutdown_function('php_fatal_handler');
PHP (Error.php)
<?php if (!defined('APPPATH')) exit('No direct script access allowed');
function error($type, $message, $file='', $line='', $url='') {
$year = date("y");
$month = date("m");
$day = date("d");
$path = APPPATH.'errors/'.$year.'-'.$month.'-'.$day.'.log';
// Prep file path
if ($file === null) {
$file = ''; // Or some other appropriate default value
}
$file = str_replace(ROOTPATH, '', $file);
$file = str_replace(BASEURL, 'public/', $file);
// Prep URL
$url = str_replace(BASEURL, '', $url);
$server_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
if (isset($server_uri) && substr($server_uri, 0, 1) == '/') {
$server_uri = substr($server_uri, 1);
}
/* Prep data --------------------------------- */
$error_data = [
'type' => $type,
'time' => date('H:i:s'),
'message' => $message,
'file' => $file,
'line' => $line,
'username' => (!empty($_SESSION['username'])) ? $_SESSION['username'] : '',
'url' => (!empty($url)) ? $url : $server_uri
];
// Ensure that no values in $error_data contain a |
$error_data = array_map(function($value) {
return is_null($value) ? $value : str_replace('|', '', $value);
}, $error_data);
// format log message into single string concatenated by |
$error_log = implode('|', $error_data);
// Remove all new lines
$error_log = trim(preg_replace('/\s\s+/', '~~', $error_log));
// Replace new lines with space
$error_log = str_replace("\r", '~~', $error_log);
$error_log = str_replace("\n", '~~', $error_log);
// Add new line to the end of the log message
$error_log = $error_log . PHP_EOL;
/* Log the error --------------------------------- */
file_write($path, $error_log);
}
function php_error_handler($errno, $errstr, $errfile, $errline) {
$message = $errno . ' - ' . $errstr;
return error('php', $message, $errfile, $errline);
}
function php_fatal_handler() {
$errfile = '';
$errstr = 'Fatal error';
$errno = E_CORE_ERROR;
$errline = 0;
$error = error_get_last();
if($error !== NULL) {
$errno = $error["type"];
$errfile = $error["file"];
$errline = $error["line"];
$errstr = $error["message"];
$message = $errno . ' - ' . $errstr;
error('php', $message, $errfile, $errline);
}
}
PHP (MySQL Error Handling)
Finally, this code sits in my MySQL library catching any errors that take place:
private function ExceptionLog($message , $sql='') {
$exception = 'Unhandled Exception. <br />';
$exception .= $message;
$exception .= "<br /> You can find the error back in the log.";
if(!empty($sql)) {
# Add the Raw SQL to the Log
$message .= "\r\nRaw SQL : " . $sql;
}
# Log error to dashboard
error('mysql', $message);
# Write into log
$this->log->write($message);
# Email error
//mail($this->settings['error_email'], 'Crafd DB Error', $exception, 'designpro@gmail.com');
return $exception;
}
And I’ve got a try/catch on connection (Note: I’ve redacted the rest of the function):
private function Connect($db_config) {
try {
# Code to attempt here
}
catch (PDOException $e) {
# Write into log
echo $this->ExceptionLog($e->getMessage());
die();
}
}
Lastly, another try/catch on execution:
private function Execute($query,$parameters = "") {
try {
# Code to attempt here
}
catch (PDOException $e) {
# Write into log and display Exception
echo $this->ExceptionLog($e->getMessage(), $query );
die();
}
}
Leave a comment