Build a complete error strategy: chained business exceptions, controlled finally and global handlers as a last line of defense.
Open this lesson in KodokonSince PHP 7, everything that gets thrown implements Throwable, with two branches: Error (engine and typing failures, like TypeError) and Exception (application failures). You only catch Error at the boundaries of the program. On the application side, the SPL provides a ready-made hierarchy: LogicException signals a developer bug, to be fixed; RuntimeException a runtime contingency, to be handled. The professional reflex: derive your business exceptions from these bases, one per identifiable situation.
<?php
declare(strict_types=1);
abstract class AppException extends RuntimeException
{
}
final class UserNotFound extends AppException
{
public static function withEmail(
string $email
): self {
return new self("No user for $email");
}
}
try {
throw UserNotFound::withEmail('lea@example.com');
} catch (UserNotFound $e) {
echo $e->getMessage();
}try/catch is only worth it if you know what to do with the error - otherwise, let it bubble up. Three tools refine the control: the multi-catch catch (JsonException | ValueError $e) groups identical handling; the previous parameter chains the original cause when you translate a technical exception into a business one; finally runs in every case - success, exception, even an early return - ideal for releasing a resource.
<?php
declare(strict_types=1);
final class PayloadInvalid extends RuntimeException
{
}
function decode(string $json): mixed
{
try {
return json_decode(
$json,
true,
flags: JSON_THROW_ON_ERROR,
);
} catch (JsonException $e) {
throw new PayloadInvalid(
'Invalid JSON payload',
previous: $e,
);
} finally {
echo "decode() finished\n";
}
}
try {
decode('{broken');
} catch (PayloadInvalid $e) {
echo $e->getPrevious()?->getMessage();
}At the end of the chain, two global handlers close the setup. set_exception_handler catches everything that wasn't handled: structured logging, a generic 500 response - without ever disclosing the message or trace to the user. set_error_handler converts legacy errors (warnings, notices) into ErrorException, unifying the two worlds: everything becomes an exception, everything follows the same handling path.
<?php
declare(strict_types=1);
set_error_handler(function (
int $severity,
string $message,
string $file,
int $line,
): bool {
throw new ErrorException(
$message,
0,
$severity,
$file,
$line,
);
});
set_exception_handler(function (Throwable $e): void {
error_log($e->getMessage());
http_response_code(500);
echo 'A technical error occurred.';
});
trigger_error('Legacy warning', E_USER_WARNING);
echo 'Never reached';Error and Exception have in common in modern PHP?Error inherits from ExceptionThrowable interfacefinally block run?previous parameter of an exception for?