Catch multiple exception types (catch (A|B $e))
Concept
PHP 8.0 added the ability to catch multiple exception types in a single catch block using the pipe syntax: catch (TypeA|TypeB $e). Previously, you needed separate catch blocks for each exception type, often with identical handling code.
Syntax: catch (ExceptionA|ExceptionB $e). Both types must be valid class names (not \Throwable itself as a shorthand — write \Exception|\Error if you want both base classes). The caught variable $e will be typed as the intersection of both types for static analysis — effectively only the shared interface (\Throwable) is usable without a type check.
When to use: When the same error handling logic applies to two different but unrelated exception types. For example: catch (\JsonException|\ParseException $e) when you want to treat JSON and XML parse errors identically.
When NOT to use: If the types share a common parent, catch the parent instead: catch (OrderException $e) instead of catch (OrderNotFoundException|OrderAlreadyShippedException $e). Multi-catch is for unrelated types that happen to need the same handling.
Combined with exception hierarchy: catch (\DatabaseException|\CacheException $e) — both are infrastructure exceptions from different subsystems; you want to treat them as "infrastructure failure" regardless of the specific subsystem.
Code Example
<?php
declare(strict_types=1);
class AuthException extends \RuntimeException {}
class SessionExpiredException extends AuthException {}
class InvalidTokenException extends AuthException {}
class NetworkException extends \RuntimeException {}
class TimeoutException extends NetworkException {}
class ConnectionRefusedException extends NetworkException {}
// Before PHP 8.0 — duplicate catch blocks
try {
fetchFromApi();
} catch (TimeoutException $e) {
$this->retryLater($e->getMessage());
} catch (ConnectionRefusedException $e) {
$this->retryLater($e->getMessage()); // identical handling
}
// PHP 8.0 — catch multiple types
try {
fetchFromApi();
} catch (TimeoutException|ConnectionRefusedException $e) {
$this->retryLater($e->getMessage()); // one block for both
}
// Real-world: different infrastructure exceptions with same recovery
function fetchUserData(int $userId): array
{
try {
return $this->cache->get("user:$userId")
?? $this->db->findUser($userId);
} catch (\Redis\ConnectionException|\PDOException $e) {
// Both are infrastructure failures — log and return empty
$this->logger->error("Infrastructure failure: " . $e->getMessage());
return [];
} catch (SessionExpiredException|InvalidTokenException $e) {
// Both need re-authentication
$this->redirectToLogin();
}
}
// Static analysis note: $e's type is the union in catch
// PHPStan/Psalm types it as TimeoutException|ConnectionRefusedException
// Methods shared by both types (from NetworkException parent) are accessible
try {
throw new TimeoutException("30s exceeded");
} catch (TimeoutException|ConnectionRefusedException $e) {
// $e is TimeoutException|ConnectionRefusedException
// getMessage() is available on both (from \Throwable)
echo $e->getMessage();
// To use type-specific methods, use instanceof check:
if ($e instanceof TimeoutException) {
// timeout-specific handling
}
}