0

Exceptions — throw, catch, finally, re-throw

Beginner5 min read·php-10-003
interviewcompare

Concept

PHP's exception system provides structured error handling via try/catch/finally blocks. Unlike traditional PHP errors (which can be suppressed with @ or handled with set_error_handler), exceptions must be explicitly caught or they propagate up the call stack.

throw: Creates and throws an exception. In PHP 8.0, throw is an expression (covered in php-09-009). The thrown value must be an instance of \Throwable (which includes \Exception and \Error).

catch ($type $e): Catches exceptions of the specified type or any subclass. Multiple catch blocks handle different types. PHP 8.0 added catch without variable: catch (SpecificException) (when you don't need the exception object).

finally: Executes regardless of whether an exception was thrown or caught. Runs even if the try or catch contains a return. Critical for cleanup: closing file handles, releasing locks, committing/rolling back transactions. If finally itself throws, the original exception is discarded.

Re-throwing: throw $e inside a catch block re-throws the caught exception. throw new WrapperException('msg', previous: $e) wraps it. The $previous parameter preserves the original exception in the chain.

Exception vs Error: \Exception is for application-level exceptional conditions. \Error is for PHP engine errors (type errors, parse errors, fatal errors). Both implement \Throwable. Catch \Throwable to catch absolutely everything.

Code Example

php
<?php
declare(strict_types=1);

// Basic try/catch/finally
function readConfig(string $path): array
{
    $fh = null;
    try {
        $fh = fopen($path, 'r');
        if ($fh === false) {
            throw new \RuntimeException("Cannot open: $path");
        }
        $content = fread($fh, 10_000);
        return json_decode($content, true, flags: JSON_THROW_ON_ERROR);
    } catch (\JsonException $e) {
        throw new \RuntimeException("Invalid JSON in $path", previous: $e);
    } finally {
        if ($fh !== false && $fh !== null) {
            fclose($fh); // ALWAYS runs — file handle closed even if exception thrown
        }
    }
}

// Multiple catch blocks
try {
    $config = readConfig('/etc/app/config.json');
} catch (\RuntimeException $e) {
    echo "Config error: " . $e->getMessage() . "\n";
    echo "Caused by: " . ($e->getPrevious()?->getMessage() ?? 'unknown') . "\n";
} catch (\Throwable $e) {
    echo "Unexpected error: " . $e->getMessage() . "\n";
}

// PHP 8.0 catch without variable
try {
    $result = riskyOperation();
} catch (\InvalidArgumentException) {
    // We don't care about the exception details, just that it happened
    $result = null;
}

// Re-throw after cleanup
function processWithTransaction(): void
{
    beginTransaction();
    try {
        doWork();
        commit();
    } catch (\Exception $e) {
        rollback();
        throw $e; // re-throw after cleanup
    }
}

// finally with return — finally runs even when returning
function withFinally(): string
{
    try {
        return "from try";
    } finally {
        echo "finally runs\n"; // runs before the return is actually made
    }
}
echo withFinally(); // prints "finally runs" then "from try"