Error Handling
Before PHP 7, "error handling" mostly meant living with whatever the interpreter printed to the screen. A missing variable produced a notice, calling a method on null produced a fatal error that couldn't be caught by anything, and a typo in a function name simply halted the script. PHP 7 changed that by introducing the Error class alongside the existing Exception class, and unifying both under a single interface: Throwable. Understanding that split — and why it exists — is the foundation for everything else in PHP's error model.
Errors, warnings, and notices: the old model
PHP has always distinguished between different severities of problems. A notice flags something questionable but survivable, like reading an undefined array key. A warning flags something more clearly wrong, like passing the wrong type to a function, but execution continues. A fatal error stops the script immediately. Historically, fatal errors were special: they weren't objects, you couldn't inspect them, and no try/catch block could intercept them. If undefinedFunction() was called, the request just died with a message dumped to the page (or the log, depending on configuration).
A fatal error before PHP 7 could not be caught
<?php
function process(array $data): void
{
// Calling a function that doesn't exist used to be an
// uncatchable fatal error, no matter how this call was wrapped.
validateData($data);
}
try {
process(['id' => 1]);
} catch (\Exception $e) {
// This never ran on PHP 5/early PHP 7 for a missing-function error --
// the process just terminated instead.
echo 'Caught: ', $e->getMessage();
}PHP 7+: Error and Exception under Throwable
PHP 7 introduced an Error class hierarchy that mirrors the existing Exception hierarchy, and gave both a common ancestor interface: Throwable. TypeError, ValueError (added in PHP 8), DivisionByZeroError, and ArgumentCountError all extend Error. InvalidArgumentException, RuntimeException, and your own custom exceptions extend Exception. Both branches are catchable with a normal try/catch, and both implement Throwable, so code that wants to catch anything that went wrong — regardless of which branch it came from — can type-hint the catch block against \\Throwable instead of \\Exception.
Error and Exception are siblings under Throwable
<?php
function divide(int $a, int $b): int
{
return intdiv($a, $b);
}
try {
echo divide(10, 0);
} catch (\DivisionByZeroError $e) {
// DivisionByZeroError extends Error, not Exception --
// but it is still fully catchable in PHP 8.
echo 'Math problem: ', $e->getMessage(), "\n";
}
try {
strlen(); // ArgumentCountError: too few arguments
} catch (\ArgumentCountError $e) {
echo 'Argument problem: ', $e->getMessage(), "\n";
}Math problem: Division by zero Argument problem: strlen() expects exactly 1 argument, 0 given
The practical effect is that the class of bugs that used to be "instant, uncatchable script death" — calling a method on null, dividing by zero, passing the wrong argument count — are now regular objects you can inspect, log, and recover from, provided you catch \\Error or \\Throwable rather than only \\Exception.
Converting legacy notices and warnings with set_error_handler()
Notices and warnings still exist as a separate mechanism from exceptions — they're raised with PHP's older trigger_error()-style machinery, not thrown. set_error_handler() lets you register a callback that intercepts these before PHP's default handler (which just prints or logs them) gets a chance. A common pattern is to convert every warning and notice into a thrown ErrorException, so that the rest of the codebase only ever has to deal with one consistent mechanism: try/catch.
Promoting warnings to exceptions
<?php
set_error_handler(function (int $severity, string $message, string $file, int $line) {
// Turn every warning/notice into a real, catchable exception.
throw new \ErrorException($message, 0, $severity, $file, $line);
});
try {
$value = $undefinedVariable; // normally just a warning
} catch (\ErrorException $e) {
echo 'Converted to exception: ', $e->getMessage(), "\n";
}
restore_error_handler();Fail loud in development, log quietly in production
The general philosophy PHP encourages — and that every serious framework follows — is environment-dependent behavior. In development, you want errors to be as loud and detailed as possible: full stack traces on screen, every notice visible, nothing swallowed silently, because a hidden bug found in development is cheap and a hidden bug found in production is expensive. In production, the opposite is true: end users should never see a stack trace (it can leak file paths, database credentials in a connection string, or internal logic), but the same failure must still be recorded somewhere the team can find it. That split is implemented with the display_errors and log_errors ini settings, covered in detail on the error reporting levels page.
Throwableis the common interface implemented by bothErrorandException-- catch it when you truly need to handle anything that can go wrong.PHP 8 turns many former fatal errors (
TypeError,DivisionByZeroError,ArgumentCountError) into catchableErrorsubclasses.set_error_handler()intercepts legacy notices/warnings and can convert them into thrownErrorExceptionobjects for consistent handling.Notices/warnings and thrown exceptions are historically separate mechanisms in PHP, even though PHP 8 narrows the gap between them.
Development should surface every error immediately; production should log everything and display nothing sensitive.