Codeskill

Learn to code, step by step

Error handling and logging properly

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.

PreviousOOP in practice – interfaces, traits, dependency injection