Errors, exceptions and logging

Error versus Exception, try/catch/finally, custom exception types, turning warnings into exceptions, log levels and production error display.

Catching what you can handle

<?php
declare(strict_types=1);

try {
    $receipt = $gateway->charge($order);
} catch (PaymentDeclined $e) {
    $log->warning("Declined", ["order" => $order->id, "code" => $e->code]);
    throw new OrderFailed("Could not charge order " . $order->id, previous: $e);
} catch (NetworkException $e) {
    $log->error("Gateway unreachable", ["exception" => $e]);
    throw $e;
} finally {
    $pdo->commit();
}
  • catch blocks are tried in order, so list the specific types first and a broad type last.
  • Catch only what you can act on. Logging and rethrowing the same exception at every layer produces three identical log lines and one real cause.
  • Pass the original exception as previous; the chain is what makes an incident readable later.
  • finally runs whether the block succeeded, threw or returned, which is why cleanup belongs there.
  • Throwable is the root type that Error and Exception share; catch it in a global handler, not in ordinary business code.

Custom exceptions and warnings as exceptions

final class PaymentDeclined extends RuntimeException
{
    public function __construct(public readonly string $code, string $message = "")
    {
        parent::__construct($message);
    }
}

final class OrderFailed extends RuntimeException
{
}

// Warnings and notices are not exceptions; convert them at the entry point
set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

try {
    json_decode($payload, flags: JSON_THROW_ON_ERROR);   // throws instead of returning null
} catch (JsonException $e) {
    $log->error("Bad payload", ["exception" => $e]);
}
SituationCorrect tool
The caller can recoverAn exception it can catch
A programming mistakeInvalidArgumentException or LogicException
A dependency failedWrap it and expose your own exception type
Something impossible happenedassert() or a LogicException
A missing file at bootFail loudly; do not continue in a half-configured state

Logging and production settings

<?php
ini_set("display_errors", "0");       // never print stack traces to visitors
ini_set("log_errors", "1");
ini_set("error_log", "/var/log/php/app.log");
error_reporting(E_ALL);

set_exception_handler(function (Throwable $e): void {
    error_log((string) $e);           // full detail to the file
    http_response_code(500);
    echo json_encode(["error" => "Internal error"]);
});

// Monolog levels, through the PSR-3 interface
$log->debug("cache key rebuilt", ["key" => $key]);
$log->info("order placed", ["id" => $id]);
$log->error("payment failed", ["exception" => $e]);
⚠️
An empty catch block is worse than a crash: the program continues with half-finished state and no record. If you truly ignore a failure, log it at debug level and write a comment explaining why it is safe.

FAQ

What is the difference between Error and Exception?
Exception is for conditions your code anticipates; Error covers engine-level problems such as calling a method on null. Both implement Throwable, so a top-level handler catches both.
Should I show errors in development?
Yes, with display_errors=1 and error_reporting=E_ALL locally so nothing hides. In production set display off and log to a file or an aggregator.

Modern PHP 8 features in practice Files, streams and the filesystem

Last refreshed 2026-09-18.