Errors and exceptions

PHP · Practical · 12 min

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:

  • Exception for application-level problems: RuntimeException, InvalidArgumentException, JsonException, PDOException and your own.
  • Error for 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 JSON

Remember

  • 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.

Check yourself

  1. Which is thrown by intdiv(1, 0)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.php.net/manual/en/language.exceptions.php