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.

Syntaxtry { /* 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.