PHPExceptions (try / catch / finally)

Exceptions (try / catch / finally)

Exceptions are PHP's mechanism for interrupting normal execution when something goes wrong, and handing control to code that's explicitly prepared to deal with it. Instead of a function returning a special "failure" value that every caller has to remember to check, it throws an object describing what went wrong, and execution jumps straight to the nearest matching catch block — skipping everything in between, no matter how deeply nested the call stack is.

The basic try / catch shape

Catching a single exception type

PHP
<?php
function withdraw(float $balance, float $amount): float
{
    if ($amount > $balance) {
        throw new \RuntimeException('Insufficient funds');
    }

    return $balance - $amount;
}

try {
    $newBalance = withdraw(100.0, 150.0);
    echo "New balance: {$newBalance}\n";
} catch (\RuntimeException $e) {
    echo 'Transaction failed: ', $e->getMessage(), "\n";
}
Transaction failed: Insufficient funds

withdraw() never had to return a sentinel value like -1 or false to signal a problem — its return type stays a clean float in the success case. The failure path is handled entirely by the throw/catch pair, which keeps the "happy path" logic readable and makes the failure impossible to accidentally ignore (an uncaught exception halts the script loudly, rather than a false return value silently propagating).

Catching multiple exception types with |

Since PHP 7.1, a single catch block can list several exception types separated by |, and it runs if the thrown object matches any of them. This is useful when different failure types should be handled identically, without resorting to catching an overly broad common ancestor.

One handler for two unrelated exception types

PHP
<?php
function loadConfig(string $path): array
{
    if (!file_exists($path)) {
        throw new \InvalidArgumentException("Config file not found: {$path}");
    }

    $contents = file_get_contents($path);
    $data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);

    return $data;
}

try {
    $config = loadConfig('/etc/app/config.json');
} catch (\InvalidArgumentException | \JsonException $e) {
    // Both a missing file and malformed JSON are "bad configuration"
    // as far as this call site is concerned.
    echo 'Could not load configuration: ', $e->getMessage(), "\n";
}
finally always runs

A finally block attached to try runs after the try/catch logic completes, regardless of whether an exception was thrown, caught, or neither. Critically, it also runs when the try or catch block contains a return statement — PHP evaluates the return value first, then runs finally, and only then actually returns.

finally running after an early return

PHP
<?php
function readAndClose(string $path): string
{
    $handle = fopen($path, 'r');

    try {
        return fread($handle, filesize($path));
    } finally {
        // Runs even though the try block already returned above.
        fclose($handle);
        echo "Handle closed\n";
    }
}

file_put_contents('/tmp/note.txt', 'hello');
echo readAndClose('/tmp/note.txt'), "\n";
Handle closed
hello

This makes finally the right place for cleanup that must happen no matter what — closing a file handle, releasing a database connection, or rolling back a lock — instead of duplicating that cleanup code once for the success path and again for every catch block.

Catching \Throwable as a last resort

\\Throwable is the interface implemented by both \\Exception and \\Error (PHP's other hierarchy, covering things like TypeError and DivisionByZeroError). Catching it means "handle absolutely anything that could go wrong here," which is appropriate at true boundaries — the outermost layer of a request handler, a background job runner, a CLI command's entry point — where the alternative is an unhandled crash, but is usually too broad for ordinary business logic.

A top-level safety net

PHP
<?php
function handleRequest(callable $action): void
{
    try {
        $action();
    } catch (\Throwable $e) {
        // Whatever it was -- Exception, Error, or a custom subtype --
        // log it and show a safe response instead of crashing.
        error_log($e->getMessage());
        http_response_code(500);
        echo 'Something went wrong.';
    }
}
Nesting try / catch

try/catch blocks can nest, and an inner catch only intercepts exceptions raised within its own try. If an inner block doesn't catch a type it isn't looking for, the exception keeps propagating outward to the next enclosing try — including one in a calling function further up the stack.

An inner catch handling one type, letting others escape

PHP
<?php
function parseAmount(string $raw): float
{
    try {
        if (!is_numeric($raw)) {
            throw new \ValueError("Not a number: {$raw}");
        }

        return (float) $raw;
    } catch (\TypeError $e) {
        // This inner block only handles TypeError -- a ValueError
        // thrown above is not caught here and keeps propagating.
        return 0.0;
    }
}

try {
    parseAmount('not-a-number');
} catch (\ValueError $e) {
    echo 'Caught at the outer level: ', $e->getMessage(), "\n";
}
Caught at the outer level: Not a number: not-a-number
An empty catch block silently discards information
`catch (\\Exception $e) ` compiles and runs fine, but it throws away the exact detail — the message, the type, the stack trace — that would have told you what went wrong. Even a catch block that decides to recover and continue should at minimum log the exception before moving on; silently swallowing it turns a debuggable failure into a mystery bug reported days later.
  • throw raises an object; execution jumps to the nearest matching catch, skipping everything in between.

  • catch (TypeA | TypeB $e) handles either type with one block.

  • finally always executes, even after a return inside try or catch, making it the right place for guaranteed cleanup.

  • Catch specific exception types close to where you can actually recover; reserve \Throwable for top-level safety nets.

  • An uncaught exception in an inner try keeps propagating to outer try blocks or, eventually, crashes the script.

Note
PHP also supports a catch block with no captured variable — `catch (\\RuntimeException) { ... }` — when you need to react to a type without inspecting its details. It's a minor syntax convenience added in PHP 8, not a different mechanism.
Tip
Resist the urge to wrap large stretches of code in one giant `try` block "to be safe." A narrow `try` around just the operation that can actually fail keeps the failure point obvious and makes it clear exactly which line the `catch` block is responding to.