0

Catch multiple exception types (catch (A|B $e))

Intermediate5 min read·php-10-006

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
<?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
    }
}