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.
| Class | Thrown when |
|---|---|
|
an engine-level error: undefined function, calling a method on |
|
a value has the wrong type for a parameter, return or property, or an operation (arithmetic on an array) |
|
too few arguments to a user function, or wrong count for an internal one |
|
an argument has the right type but an invalid value ( |
|
shifts by a negative amount, integer overflow in |
|
no arm of a |
|
|
|
syntax or compile errors in code loaded with |
|
|
|
wraps a classic PHP error, typically thrown from an error handler |
|
a bug in the calling code: invalid argument, domain or length, an out-of-range key known in advance |
|
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.
Source: Errors in PHP 7, Predefined Exceptions and SPL Exceptions
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
Source: Extending Exceptions and The Exception class
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 |
|---|---|
|
fatal: execution stops (most user-visible fatal errors are now |
|
parse error |
|
something is wrong, execution continues: undefined variable or array key, failed |
|
minor issues (few remain in PHP 8) |
|
uses a feature scheduled for removal |
|
triggered by |
|
legacy catchable fatal error |
|
every level (since 8.0 including |
The settings that matter:
-
error_reporting— which levels are reported. UseE_ALLeverywhere; the production ini usesE_ALL & ~E_DEPRECATED. -
display_errors— print errors in the output.Onin development only; in production it leaks paths and code. -
log_errorsanderror_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 (Onin 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}
Source: RFC: Error Backtraces v2 and Runtime configuration
Good practice
-
Throw exceptions for exceptional conditions in your own code; never
die()orexit()with a message from library code. -
Enable
E_ALLin 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, usePDO::ERRMODE_EXCEPTION(the default since 8.0) andmysqli'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
Errorthrowables, and since 8.0 many warnings (strlen([]), invalid arguments) areTypeError/ValueError. Books written for PHP 7.0-7.3 (Beak ch. 11) list warnings that are exceptions now. -
die("Fatal Error")error handling (Nixon) andor die(mysql_error())idioms should be replaced by exceptions. -
E_STRICTno longer exists as a separate level (8.0);E_USER_ERRORviatrigger_error()is deprecated (8.4). -
$php_errormsgandtrack_errorswere removed in 8.0. -
get_error_handler(), fatal-error backtraces (8.5) and the 8.6 deprecation of returning fromfinallyare new.