PHPError Handling

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
<?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
<?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
<?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();
Note
`set_error_handler()` only intercepts the legacy error mechanism (notices, warnings, and deprecation messages). It has no effect on objects that are already thrown as `Error` or `Exception` instances — those are handled by `try`/`catch` directly, and by `set_exception_handler()` for ones that escape every catch block.
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.

Never let raw exception output reach end users in production
An uncaught exception's default rendering includes the full file path, line number, and sometimes a portion of the call stack. On a production server this is information disclosure, not just an ugly page. Always pair production error handling with a top-level `set_exception_handler()` (or your framework's equivalent) that logs the real error and shows the user a generic message instead.
  • Throwable is the common interface implemented by both Error and Exception -- catch it when you truly need to handle anything that can go wrong.

  • PHP 8 turns many former fatal errors (TypeError, DivisionByZeroError, ArgumentCountError) into catchable Error subclasses.

  • set_error_handler() intercepts legacy notices/warnings and can convert them into thrown ErrorException objects 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.

Tip
When in doubt about which base type to catch, catch the most specific type your code can actually recover from (`\\DivisionByZeroError`, `\\InvalidArgumentException`), and let anything else propagate up to a single top-level handler that logs it. Catching `\\Throwable` everywhere "just in case" tends to hide real bugs instead of fixing them.