Exceptions — throw, catch, finally, re-throw
Concept
PHP's exception system provides structured error handling via try/catch/finally blocks. Unlike traditional PHP errors (which can be suppressed with @ or handled with set_error_handler), exceptions must be explicitly caught or they propagate up the call stack.
throw: Creates and throws an exception. In PHP 8.0, throw is an expression (covered in php-09-009). The thrown value must be an instance of \Throwable (which includes \Exception and \Error).
catch ($type $e): Catches exceptions of the specified type or any subclass. Multiple catch blocks handle different types. PHP 8.0 added catch without variable: catch (SpecificException) (when you don't need the exception object).
finally: Executes regardless of whether an exception was thrown or caught. Runs even if the try or catch contains a return. Critical for cleanup: closing file handles, releasing locks, committing/rolling back transactions. If finally itself throws, the original exception is discarded.
Re-throwing: throw $e inside a catch block re-throws the caught exception. throw new WrapperException('msg', previous: $e) wraps it. The $previous parameter preserves the original exception in the chain.
Exception vs Error: \Exception is for application-level exceptional conditions. \Error is for PHP engine errors (type errors, parse errors, fatal errors). Both implement \Throwable. Catch \Throwable to catch absolutely everything.
Code Example
<?php
declare(strict_types=1);
// Basic try/catch/finally
function readConfig(string $path): array
{
$fh = null;
try {
$fh = fopen($path, 'r');
if ($fh === false) {
throw new \RuntimeException("Cannot open: $path");
}
$content = fread($fh, 10_000);
return json_decode($content, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \RuntimeException("Invalid JSON in $path", previous: $e);
} finally {
if ($fh !== false && $fh !== null) {
fclose($fh); // ALWAYS runs — file handle closed even if exception thrown
}
}
}
// Multiple catch blocks
try {
$config = readConfig('/etc/app/config.json');
} catch (\RuntimeException $e) {
echo "Config error: " . $e->getMessage() . "\n";
echo "Caused by: " . ($e->getPrevious()?->getMessage() ?? 'unknown') . "\n";
} catch (\Throwable $e) {
echo "Unexpected error: " . $e->getMessage() . "\n";
}
// PHP 8.0 catch without variable
try {
$result = riskyOperation();
} catch (\InvalidArgumentException) {
// We don't care about the exception details, just that it happened
$result = null;
}
// Re-throw after cleanup
function processWithTransaction(): void
{
beginTransaction();
try {
doWork();
commit();
} catch (\Exception $e) {
rollback();
throw $e; // re-throw after cleanup
}
}
// finally with return — finally runs even when returning
function withFinally(): string
{
try {
return "from try";
} finally {
echo "finally runs\n"; // runs before the return is actually made
}
}
echo withFinally(); // prints "finally runs" then "from try"