Errors and Exceptions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against PHP 8.5, checked on 2026-10-05. PHP has two overlapping mechanisms: exceptions, thrown and caught like in other languages, and errors, the older diagnostics (warnings, notices, deprecations) that are reported and logged but do not interrupt execution. Since PHP 7 most fatal errors are thrown as Error exceptions, and since PHP 8 many warnings became TypeError or ValueError.

The Throwable hierarchy

Everything that can be thrown implements the Throwable interface, which user classes cannot implement directly: extend Exception (or one of its subclasses) instead. Error and its subclasses represent problems in the program itself, usually bugs; Exception and its subclasses represent conditions a program can anticipate and handle.

classDiagram direction TB class Throwable { <<interface>> getMessage() getCode() getPrevious() getTrace() } Throwable <|.. Error Throwable <|.. Exception Error <|-- TypeError TypeError <|-- ArgumentCountError Error <|-- ValueError Error <|-- ArithmeticError ArithmeticError <|-- DivisionByZeroError Error <|-- AssertionError Error <|-- CompileError CompileError <|-- ParseError Error <|-- UnhandledMatchError Error <|-- FiberError Exception <|-- ErrorException Exception <|-- JsonException Exception <|-- LogicException Exception <|-- RuntimeException LogicException <|-- InvalidArgumentException LogicException <|-- DomainException LogicException <|-- LengthException LogicException <|-- OutOfRangeException LogicException <|-- BadFunctionCallException BadFunctionCallException <|-- BadMethodCallException RuntimeException <|-- OutOfBoundsException RuntimeException <|-- OverflowException RuntimeException <|-- UnderflowException RuntimeException <|-- RangeException RuntimeException <|-- UnexpectedValueException
Class Thrown when

Error

an engine-level error: undefined function, calling a method on null, uninitialised typed property, instantiating an abstract class

TypeError

a value has the wrong type for a parameter, return or property, or an operation (arithmetic on an array)

ArgumentCountError

too few arguments to a user function, or wrong count for an internal one

ValueError

an argument has the right type but an invalid value (array_chunk($a, 0), an empty separator for explode())

ArithmeticError, DivisionByZeroError

shifts by a negative amount, integer overflow in intdiv(), division or modulo by zero

UnhandledMatchError

no arm of a match matches

AssertionError

assert() fails with assertions enabled (zend.assertions=1)

ParseError, CompileError

syntax or compile errors in code loaded with include/eval

JsonException

json_encode()/json_decode() fail with JSON_THROW_ON_ERROR

ErrorException

wraps a classic PHP error, typically thrown from an error handler

LogicException family (SPL)

a bug in the calling code: invalid argument, domain or length, an out-of-range key known in advance

RuntimeException family (SPL)

conditions only detectable at runtime: out of bounds, overflow, unexpected value

Extensions add their own: PDOException, DateMalformedStringException (8.3, a DateException), Random\RandomException, Uri\InvalidUriException (8.5) and others.

try, catch and finally

try runs code; each catch names one or more types (|, the multi-catch, 7.1) and runs for the first matching throwable; finally runs whether or not something was thrown, even after return. Since 8.0 the variable can be omitted when it is not used.

<?php

declare(strict_types=1);

function loanDays(string $format): int
{
    return match ($format) {
        'hardcover', 'paperback' => 21,
        'ebook' => 14,
    };
}

foreach (['ebook', 'vinyl'] as $format) {
    try {
        $days = loanDays($format);              // may throw before anything is printed
        echo "{$format}: {$days} days\n";
    } catch (UnhandledMatchError $e) {
        echo "{$format}: not lendable ({$e->getMessage()})\n";
    } finally {
        echo "-- checked {$format}\n";
    }
}

try {
    json_decode('{"title": ', flags: JSON_THROW_ON_ERROR);
} catch (JsonException | ValueError) {           // multi-catch, no variable (8.0)
    echo "invalid JSON\n";
}
ebook: 14 days
-- checked ebook
vinyl: not lendable (Unhandled match case 'vinyl')
-- checked vinyl
invalid JSON

Catch the most specific type you can handle, and let everything else propagate. Catching Throwable or Exception belongs at the top of the application (in a front controller, a job worker, a CLI command), where errors are logged and turned into a response.

Returning from a finally block overrides any return or exception from try, silently discarding exceptions. Upcoming in PHP 8.6, it is deprecated ("Returning from a finally block is deprecated").

Source: Exceptions

Custom exceptions and chaining

Define exception classes for the conditions callers may want to handle separately, extending the closest SPL or standard exception. Pass the original exception as $previous when translating a low-level error into a domain one, so that the root cause stays available in logs (getPrevious()).

<?php

declare(strict_types=1);

final class BookNotAvailable extends RuntimeException
{
    public static function becauseOnLoan(string $isbn, ?Throwable $previous = null): self
    {
        return new self("Book {$isbn} is on loan", 409, $previous);
    }
}

function lend(string $isbn, array $onLoan): string
{
    try {
        if (in_array($isbn, $onLoan, true)) {
            throw new DomainException('loan record exists');
        }
        return "lent {$isbn}";
    } catch (DomainException $e) {
        throw BookNotAvailable::becauseOnLoan($isbn, $e);     // chain the cause
    }
}

try {
    lend('978-0-00-000001-1', ['978-0-00-000001-1']);
} catch (BookNotAvailable $e) {
    printf("%s (code %d), caused by: %s\n", $e->getMessage(), $e->getCode(), $e->getPrevious()?->getMessage());
}
// Book 978-0-00-000001-1 is on loan (code 409), caused by: loan record exists

Classic errors: levels and reporting

Warnings, notices and deprecations are not exceptions: PHP reports them (according to error_reporting), then continues. The levels are bit flags:

Constant Meaning

E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR

fatal: execution stops (most user-visible fatal errors are now Error exceptions instead)

E_PARSE

parse error

E_WARNING, E_CORE_WARNING, E_COMPILE_WARNING

something is wrong, execution continues: undefined variable or array key, failed fopen()

E_NOTICE

minor issues (few remain in PHP 8)

E_DEPRECATED

uses a feature scheduled for removal

E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED

triggered by trigger_error(); E_USER_ERROR is deprecated since 8.4 (throw an exception instead)

E_RECOVERABLE_ERROR

legacy catchable fatal error

E_ALL

every level (since 8.0 including E_STRICT, which no longer exists as a separate level)

The settings that matter:

  • error_reporting — which levels are reported. Use E_ALL everywhere; the production ini uses E_ALL & ~E_DEPRECATED.

  • display_errors — print errors in the output. On in development only; in production it leaks paths and code.

  • log_errors and error_log — write errors to a log file, syslog, or the SAPI’s log (stderr for the CLI and FPM).

  • zend.exception_ignore_args — omit argument values from stack traces (On in production).

; development
error_reporting = E_ALL
display_errors = On
log_errors = On

; production
error_reporting = E_ALL & ~E_DEPRECATED
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
zend.exception_ignore_args = On

Error and exception handlers

set_error_handler() installs a callback for classic errors; it receives the level, message, file and line, and returns true to stop PHP’s standard handling. The common modern pattern converts every reported error into an ErrorException, so that warnings stop execution and can be caught like exceptions. set_exception_handler() installs the last-resort handler for uncaught throwables. Since PHP 8.5, get_error_handler() and get_exception_handler() return the current handlers without replacing them.

<?php

declare(strict_types=1);

set_error_handler(static function (int $level, string $message, string $file, int $line): bool {
    if (!(error_reporting() & $level)) {
        return false;                       // respect error_reporting and the @ operator
    }
    throw new ErrorException($message, 0, $level, $file, $line);
});

set_exception_handler(static function (Throwable $e): void {
    error_log(sprintf('[%s] %s in %s:%d', $e::class, $e->getMessage(), $e->getFile(), $e->getLine()));
    fwrite(STDERR, "Something went wrong; the incident was logged.\n");
});

var_dump(get_error_handler() instanceof Closure);   // bool(true) -- 8.5

try {
    $titles = ['Kindred'];
    echo $titles[3];                                 // warning -> ErrorException
} catch (ErrorException $e) {
    echo get_class($e), ': ', $e->getMessage(), ' (', $e->getSeverity() === E_WARNING ? 'E_WARNING' : 'other', ")\n";
}
// ErrorException: Undefined array key 3 (E_WARNING)

Frameworks install these handlers for you: Laravel turns errors into exceptions and renders them with its exception handler (see Error Handling and Logging). error_log() writes a message to the configured log; error_get_last() returns the last error that occurred.

Fatal error backtraces (PHP 8.5)

The remaining true fatal errors (out of memory, maximum execution time, some compile errors raised at runtime) used to show only a message and location. Since PHP 8.5 they include a stack trace, controlled by the fatal_error_backtraces ini setting (enabled by default):

<?php

declare(strict_types=1);

function registerFormat(): void
{
    new class implements Countable {};    // compile error raised while running: missing count()
}

function boot(): void
{
    registerFormat();
}

boot();
Fatal error: Class Countable@anonymous must implement 1 abstract method (Countable::count) in ... on line 7
Stack trace:
#0 ...(12): registerFormat()
#1 ...(15): boot()
#2 {main}

Good practice

  • Throw exceptions for exceptional conditions in your own code; never die() or exit() with a message from library code.

  • Enable E_ALL in every environment, convert errors to exceptions, and fix deprecations as they appear: they are the upgrade path to the next PHP version.

  • Pass JSON_THROW_ON_ERROR, use PDO::ERRMODE_EXCEPTION (the default since 8.0) and mysqli's exception mode (the default since 8.1), so failures cannot be silently ignored.

  • Log with context (a PSR-3 logger such as Monolog), show users a generic message, and never display stack traces in production.

Where older books and tutorials are outdated

  • Fatal errors that could not be caught: since PHP 7 they are Error throwables, and since 8.0 many warnings (strlen([]), invalid arguments) are TypeError/ValueError. Books written for PHP 7.0-7.3 (Beak ch. 11) list warnings that are exceptions now.

  • die("Fatal Error") error handling (Nixon) and or die(mysql_error()) idioms should be replaced by exceptions.

  • E_STRICT no longer exists as a separate level (8.0); E_USER_ERROR via trigger_error() is deprecated (8.4).

  • $php_errormsg and track_errors were removed in 8.0.

  • get_error_handler(), fatal-error backtraces (8.5) and the 8.6 deprecation of returning from finally are new.