Exception chaining — $previous parameter
Concept
Exception chaining allows you to wrap one exception in another while preserving the original as the "previous" exception. This creates a chain of causality — you can trace exactly what caused what, even across layer boundaries.
The $previous parameter: Every \Exception and \Error constructor accepts a \Throwable $previous = null parameter. Pass the original exception as previous when wrapping it in a higher-level exception.
Why chaining matters: In a layered architecture, low-level exceptions (like \PDOException) should not bubble up to the HTTP layer — they expose database details. But you still need the original exception for debugging. Wrapping: throw new DatabaseException("User save failed", previous: $pdoException) gives callers a meaningful high-level exception while preserving the original for logging.
$e->getPrevious(): Returns the previous exception (or null). getPrevious() returns a \Throwable, so you can chain multiple levels.
Stack traces and chains: When you log an exception chain, log all levels. Monolog and Laravel's built-in logging will trace through getPrevious() to include the full chain. Sentry shows exception chains as a sequence.
Code Example
<?php
declare(strict_types=1);
// Exception chaining — wrap low-level exception in high-level
class UserRepository
{
public function save(array $user): void
{
try {
$this->db->execute("INSERT INTO users ...", $user);
} catch (\PDOException $e) {
// Don't leak PDO details to higher layers
throw new \RuntimeException(
"Failed to save user: {$user['email']}",
previous: $e // original PDO exception preserved
);
}
}
}
// Caller can catch high-level but still access the original
try {
$repo->save(['email' => 'alice@example.com']);
} catch (\RuntimeException $e) {
echo "High-level: " . $e->getMessage() . "\n";
$original = $e->getPrevious();
if ($original instanceof \PDOException) {
echo "DB cause: " . $original->getMessage() . "\n";
echo "SQL state: " . $original->getCode() . "\n";
}
}
// Full chain logging
function logExceptionChain(\Throwable $e): void
{
$depth = 0;
$current = $e;
while ($current !== null) {
error_log(sprintf(
"[%d] %s: %s in %s:%d",
$depth++,
$current::class,
$current->getMessage(),
$current->getFile(),
$current->getLine()
));
$current = $current->getPrevious();
}
}
// Collecting all exceptions in a chain
function getExceptionChain(\Throwable $e): array
{
$chain = [];
$current = $e;
while ($current !== null) {
$chain[] = $current;
$current = $current->getPrevious();
}
return $chain;
}
// Deep chain example
try {
try {
throw new \PDOException("Connection refused");
} catch (\PDOException $db) {
throw new \RuntimeException("DB operation failed", previous: $db);
}
} catch (\RuntimeException $e) {
throw new \LogicException("Service unavailable", previous: $e);
}
// When catching, e->getPrevious()->getPrevious() would give the PDOException