Robust code fails loudly and predictably, and modern PHP reports most problems as exceptions.
Signal a failure with throw new InvalidArgumentException('...') and handle it with try/catch/finally. finally runs whether or not an exception was thrown, which makes it the place to release resources.
Everything you can throw implements Throwable, which has two branches:
Exceptionfor application-level problems:RuntimeException,InvalidArgumentException,JsonException,PDOExceptionand your own.Errorfor engine-level problems:TypeError,ValueError,ArgumentCountError,DivisionByZeroError,UnhandledMatchError. These usually mean a bug.
Catch the most specific type you can handle. You can catch several at once, catch (A | B $e), and since PHP 8.0 omit the variable when you don't need it: catch (JsonException). Catching Throwable belongs only at the top level (a framework's error handler or a worker loop) where you log and respond.
Create domain exceptions by extending a built-in one, and chain the original cause as previous so stack traces aren't lost:
throw new PaymentFailed('Card declined', previous: $e);throw is an expression since PHP 8.0: $user = $repo->find($id) ?? throw new NotFound();.
Older functions still report problems as warnings and return false. Prefer flags that make them throw, like json_decode($s, flags: JSON_THROW_ON_ERROR).
Example
<?php
class InsufficientFunds extends RuntimeException {}
function withdraw(int $balance, int $amount): int {
if ($amount <= 0) throw new InvalidArgumentException('amount must be positive');
if ($amount > $balance) throw new InsufficientFunds("short by " . ($amount - $balance));
return $balance - $amount;
}
foreach ([30, 150, -1] as $amt) {
try {
echo withdraw(100, $amt), "\n"; // 70
} catch (InsufficientFunds $e) {
echo "Declined: {$e->getMessage()}\n"; // Declined: short by 50
} catch (InvalidArgumentException | TypeError $e) {
echo get_class($e), "\n"; // InvalidArgumentException
} finally {
echo "-- done\n";
}
}
try { json_decode('{bad', flags: JSON_THROW_ON_ERROR); }
catch (JsonException) { echo "bad JSON\n"; } // bad JSONRemember
- Throwable splits into Exception (app problems) and Error (engine problems)
- Catch specific types; keep catch (Throwable) for top-level handlers
- Pass previous: $e when wrapping to keep the original cause
Watch out for
- Swallowing exceptions with an empty catch block, hiding real failures.