PHP Error Handling: Errors, Exceptions, and Logging in PHP 8

Modern PHP error handling – try/catch with Throwable, custom exceptions, converting warnings to exceptions, and production display/logging settings.

error handling php

Last updated: July 2026 — this guide replaces the old Part 1 and Part 2 of this series

PHP 8 made error handling dramatically cleaner than the PHP 5 era this article originally covered: most fatal conditions are now real exceptions you can catch. Here’s the current, complete picture.

The Throwable hierarchy

Since PHP 7, everything throwable descends from Throwable, in two branches: Error (engine problems — TypeErrorDivisionByZeroError) and Exception (application problems). PHP 8 upgraded many former warnings into Error exceptions.

try {
    $result = intdiv(10, 0);
} catch (DivisionByZeroError $e) {
    // engine-level Error — catchable since PHP 7
} catch (Exception $e) {
    // application exceptions
} finally {
    // always runs — close handles, release locks
}

Catch Throwable only at your top-level handler; everywhere else, catch the most specific type you can actually deal with.

Custom exceptions

class PaymentFailedException extends RuntimeException
{
    public function __construct(
        public readonly string $orderId,
        string $message = 'Payment failed',
    ) {
        parent::__construct($message);
    }
}

throw new PaymentFailedException(orderId: 'ORD-1042');

Domain-named exceptions make catch blocks read like business rules and carry structured context to your logs.

Warnings are not exceptions — unless you convert them

Legacy functions still emit warnings instead of throwing. The standard trick turns them into exceptions:

set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline) {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

Now fopen() on a missing file throws a catchable ErrorException instead of limping on with false.

Development vs production settings

; development
display_errors = On
error_reporting = E_ALL

; production
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log

Displaying errors in production leaks file paths and query fragments to visitors — the classic misconfiguration. Log instead, and register a last-resort handler so nothing dies silently:

set_exception_handler(function (Throwable $e) {
    error_log($e);
    http_response_code(500);
    echo 'Something went wrong.';
});

In frameworks

Laravel wires all of this for you (bootstrap/app.php → withExceptions(), plus report()/rescue() helpers) — write custom exceptions and let the framework render them. The concepts above are what those helpers are built on.

Rule of thumb: throw exceptions for anything the current function can’t fix, catch them where you can genuinely respond, log everything at the boundary, and never show internals to users.continue!

Comments

comments