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 — TypeError, DivisionByZeroError) 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!
