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();
}catchblocks 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. finallyruns whether the block succeeded, threw or returned, which is why cleanup belongs there.Throwableis the root type thatErrorandExceptionshare; 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]);
}| Situation | Correct tool |
|---|---|
| The caller can recover | An exception it can catch |
| A programming mistake | InvalidArgumentException or LogicException |
| A dependency failed | Wrap it and expose your own exception type |
| Something impossible happened | assert() or a LogicException |
| A missing file at boot | Fail 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.Related
Modern PHP 8 features in practice Files, streams and the filesystem
Last refreshed 2026-09-18.