try, catch and finally
try runs code that may fail, catch handles one named class of failure, and finally runs on every way out of the block.
try { /* code */ } catch (FirstException | SecondException $e) { /* handle */ } finally { /* cleanup */ }try marks code that might throw. catch names the failure you are prepared to handle and says what to do. finally holds cleanup that must happen however the block ends.
Specific before general
PHP checks the catch blocks top to bottom and runs the first whose class matches, counting parent classes. A broad class listed first therefore shadows every narrower one below it, and the narrow block becomes dead code. DivisionByZeroError goes above ArithmeticError, never under it.
finally
finally runs on every exit: normal completion, a return inside try, a caught exception, and an exception still travelling upwards. That makes it the place to close a file handle or release a lock, because it runs even when the failure is not yours to handle.
Catching Throwable and carrying on
catch (Throwable $e) matches everything, including a TypeError from your own bug. Swallowing that and continuing leaves the program working on half-built data, and the symptom surfaces somewhere unrelated, hours later. Catch Throwable only at a boundary where you can be honest about it: log it, return a 500, stop.
Two shorthands
catch (JsonException | DomainException $e) is the same as two catch blocks with identical bodies. Since PHP 8.0 the variable may be left out, so catch (JsonException) is the same as catch (JsonException $e) where $e is never used.
Example
When to use it
- Fall back to a default when a config file contains broken JSON.
- Close a file handle or roll back a transaction whether the work succeeded or threw.
- Turn any uncaught failure into a logged 500 page at the entry point of the application.
More examples
Multi-class catch, and the same thing written long
Reach for the pipe form when two unrelated classes deserve one response, which is common around parsing. Both spellings compile to the same thing, so recognising each in someone else's code matters more than preferring one. Dropping the variable is worth doing only when the body truly never reads it, because losing the message is how a failure becomes a mystery.
<?php
declare(strict_types=1);
// Longhand: one block per class, bodies identical.
function readSettings(string $raw): array
{
try {
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new DomainException('settings must be a JSON object');
}
return $data;
} catch (JsonException $e) {
return ['theme' => 'light'];
} catch (DomainException $e) {
return ['theme' => 'light'];
}
}
// Shorthand: the pipe lists both classes for one block. Same behaviour.
// And since PHP 8.0 the variable may be dropped when the body ignores it.
function readSettingsShort(string $raw): array
{
try {
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
return is_array($data) ? $data : throw new DomainException('not an object');
} catch (JsonException | DomainException) {
return ['theme' => 'light'];
}
}finally closing a handle
There is no catch here on purpose. This code cannot decide what a malformed CSV means, so it lets the exception travel on to whoever can. The finally block still runs on the way past, and the handle closes. This is the pattern for anything you must hand back: a file, a lock file, a transaction you have to roll back.
<?php
declare(strict_types=1);
$handle = fopen('orders.csv', 'rb');
if ($handle === false) {
throw new RuntimeException('orders.csv could not be opened');
}
try {
// Since PHP 8.4 relying on the default $escape is deprecated, so pass it.
// An empty string disables PHP's own escaping, which is what RFC 4180 CSV
// expects.
while (($row = fgetcsv($handle, null, ',', '"', '')) !== false) {
if (count($row) !== 3) {
throw new UnexpectedValueException('a row does not have three columns');
}
// process the row
}
} finally {
fclose($handle);
}One Throwable catch, at the edge
The front controller is the one place catching Throwable is right: past it there is nothing but PHP's own fatal error page. Note what the handler does and does not do. It logs the detail, shows the visitor a fixed sentence with no message or path in it, and sets a 500 so monitoring and search engines see a failure. It does not continue as though nothing happened.
<?php
declare(strict_types=1);
try {
$body = handleRequest($_SERVER['REQUEST_URI'] ?? '/');
echo htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
} catch (Throwable $e) {
error_log($e::class . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
http_response_code(500);
echo 'Something went wrong. Please try again.';
}
Discussion