Error handling and logging properly. Beginner PHP often uses display_errors=On and dumps stack traces to the browser. That is fine locally. It is a security problem in production.
Development vs production
- Development – show errors, verbose logging, fast feedback
- Production – hide errors from users, log details to a file or service
Configure this in php.ini or per-directory. Locally you might leave display on. On a live server: display_errors = Off, log_errors = On.
Exceptions instead of die()
Use exceptions for exceptional failures – database down, missing config, invalid state. Catch them at the edges (front controller) and show a generic message.
<?php
declare(strict_types=1);
namespace AppServices;
use PDO;
use PDOException;
use RuntimeException;
class UserRepository
{
public function __construct(private PDO $db) {}
public function findById(int $id): array
{
try {
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
throw new RuntimeException('Could not fetch user', 0, $e);
}
if (!$row) {
throw new RuntimeException('User not found');
}
return $row;
}
}
A simple logger
You do not need Monolog on day one (though Composer makes it easy later). A small class that appends to storage/logs/app.log is enough:
<?php
declare(strict_types=1);
namespace AppServices;
class Logger
{
public function __construct(private string $path) {}
public function error(string $message, array $context = []): void
{
$this->write('ERROR', $message, $context);
}
public function info(string $message, array $context = []): void
{
$this->write('INFO', $message, $context);
}
private function write(string $level, string $message, array $context): void
{
$line = sprintf(
"[%s] %s: %s %sn",
date('c'),
$level,
$message,
$context ? json_encode($context) : ''
);
file_put_contents($this->path, $line, FILE_APPEND | LOCK_EX);
}
}
Global exception handler
Register this early in public/index.php:
set_exception_handler(function (Throwable $e) use ($logger) {
$logger->error($e->getMessage(), [
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
http_response_code(500);
if ($config['debug'] ?? false) {
echo '<pre>' . Htmlspecialchars($e->getMessage()) . '</pre>';
} else {
echo 'Something went wrong. Please try again later.';
}
});
What to log
- Failed logins (without logging passwords)
- Exceptions and stack traces (in the log file, not the HTML)
- Slow queries or external API failures
- Not every successful page view – that gets noisy fast
Good errors fail quietly for the user and loudly in the log. That is the habit professional PHP gets right early.

