skip to content

Catching & Rethrowing

Handling a thrown value in PHP: try/catch/finally, multi-catch with |, and rethrowing with $previous to keep the cause. Interviewers probe finally overriding a return and over-broad catches.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In PHP, how do try, catch and finally blocks work together, and in what order are multiple catch blocks tried?

level: juniorimportance: must knowfreq 74%

answer

  1. try needs a catch or a finally
  2. source order, first match wins
  3. match means instanceof the named type
  4. one catch, several types joined by |
  5. finally runs on the way out

basics

~20 s

Code in try runs until something throws; PHP then tests catch blocks top to bottom and runs the first whose type matches the thrown object. finally runs afterwards, whether the try succeeded, was caught, or is still propagating.

solid answer

~50 s

A `try` block must be followed by at least one `catch` or a `finally`. When a statement inside `try` throws, the rest of the block is skipped and PHP tests each `catch` in source order; the first whose listed class or interface the object is an `instanceof` handles it, and the later blocks are ignored — so specific types go above general ones. One `catch` can list several unrelated types with `|`, as in `catch (TimeoutException | ConnectionException $e)`. If no `catch` matches, the throwable keeps travelling up to the caller. `finally` runs after `try` and any `catch` on the way out — after success, after a handled exception, or while an unhandled one propagates — which makes it the place for cleanup such as releasing a lock or closing a file handle. The one everyday exit it does not survive is `exit()`.

code

php · 19 lines
php
<?php
declare(strict_types=1);

function readConfig(string $path): array
{
    $handle = fopen($path, 'r');
    if ($handle === false) {
        throw new RuntimeException("Cannot open {$path}");
    }

    try {
        $raw = stream_get_contents($handle);
        return json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new UnexpectedValueException("Bad JSON in {$path}", previous: $e);
    } finally {
        fclose($handle); // runs on success, on the rethrow, on any other failure
    }
}

go deeper

for a junior

Recall the three blocks, that a try needs a catch or a finally, and that the first matching catch in source order handles the object.

for a middle

Explain first-match versus best-match ordering, how | lists several types, and walk through each way a try can end and whether finally runs.

for a senior

Show where finally belongs in real code — locks, handles, temporary state — and name the exits it does not survive, such as exit() and memory exhaustion.

for a principal

Argue for a codebase convention: narrow catches ordered specific to general, try/finally for cleanup without handling, and no control flow inside finally.

## The three blocks PHP's exception syntax has three parts, and a `try` must be followed by **at least one** `catch` or a `finally` (or both): - **`try`** wraps the code that might fail. When any statement inside it throws, the remaining statements of the block are skipped immediately. - **`catch (Type $e)`** declares which thrown objects it handles. The object must implement `Throwable`; the name in parentheses can be a class or an interface, and `$e` receives the object. - **`finally`** holds code that must run however the `try` ends: after normal completion, after a `catch` handled something, or while an exception is still on its way up the call stack. If nothing is thrown, every `catch` is skipped and execution continues after the last block of the statement (after running `finally`, if there is one). ## How PHP chooses a catch block When something is thrown, PHP walks the `catch` clauses of the innermost enclosing `try` **in the order they are written**: 1. Take the first `catch` clause. 2. If the thrown object is an `instanceof` any type the clause names, run that block and stop looking. 3. Otherwise move to the next clause. 4. If no clause in this `try` matches, run its `finally` (if any) and let the object continue to the next enclosing `try` — in the same function or in a caller. The important word is **first**, not **best**. PHP does not look for the most specific match. With `catch (Exception $e)` written above `catch (RuntimeException $e)`, a `RuntimeException` is handled by the first block, because it *is* an `Exception`; the second block can never run. PHP does not reject such an unreachable clause, so the ordering mistake compiles and runs silently. The rule of thumb: **most specific types first, most general last**. If the object climbs all the way out of the script without a match, it becomes an uncaught error — what happens then is the business of the global exception handler, not of `try`/`catch`. ## Multi-catch with the pipe A single clause can name several types separated by `|`: ```php try { $receipt = $gateway->charge($order); } catch (TimeoutException | ConnectionException $e) { $this->retryLater($order); } ``` The block runs if the object is an instance of **any** of the listed types. This is for types from different branches of the class tree that you handle identically; if they share a parent you would catch anyway, name the parent instead. Inside the block `$e` has whichever concrete type was thrown. ## When finally runs, and when it does not | How the `try` ends | Does `finally` run? | What happens next | |---|---|---| | Completes normally | Yes | Execution continues after the statement | | Throws, a `catch` handles it | Yes, after the `catch` | Execution continues after the statement | | Throws, no `catch` matches | Yes | The exception keeps propagating | | `return` inside `try` or `catch` | Yes, before the function returns | The pending value is returned | | A `catch` rethrows | Yes | The rethrown exception propagates | | `exit()` is called | **No** | The script ends | Two caveats sit outside the table. `exit()` skips `finally` blocks entirely — php-src's own tests pin this behaviour and mark it as something that may change in a future release. And an engine fatal error that is not a throwable, such as exhausting `memory_limit`, ends the script without unwinding, so no `finally` runs either. ## Typical uses and mistakes Good uses of `finally`: - releasing a lock or a semaphore acquired before the `try`; - closing a file or stream handle opened for the operation; - restoring state that the block changed temporarily, such as a working directory. Mistakes interviewers listen for: - ordering a general `catch (Exception $e)` above a specific one and wondering why the specific block never runs; - assuming `finally` runs only when an exception occurred — it runs on success too; - putting a `return` inside `finally`, which overrides the value from `try` and silently discards a propagating exception; - expecting `catch (Exception $e)` to handle engine errors such as `TypeError`, which extend `Error`, not `Exception`.

  • If catch (Exception $e) is written before catch (InvalidArgumentException $e), does PHP warn about the unreachable second block?
    No. PHP does not check whether a later `catch` clause is reachable; it simply tests clauses in order at run time and the first match wins. An `InvalidArgumentException` is an `Exception`, so the first block handles it and the second never runs. Static analysis can point such ordering out, but the language itself accepts it.
  • Can a try block have a finally but no catch at all, and what is that useful for?
    Yes. `try { ... } finally { ... }` is valid and handles nothing: any exception keeps propagating to the caller, but the `finally` code runs first. It is the right shape when a function must clean up — release a lock, close a handle — yet has no business deciding what a failure means.
  • Does a finally block run when the try block calls exit()?
    No. `exit()` ends the script without running pending `finally` blocks, and a `catch (Throwable $e)` does not intercept it either. php-src's tests record this as current behaviour that may change later. Cleanup that must happen at script end belongs in a shutdown function or a destructor, not only in `finally`.

saying these in an interview costs you the question

  • PHP picks the most specific matching catch block, whatever the order
  • finally only runs when an exception was thrown
  • Every try block needs at least one catch block
  • A multi-catch clause matches only objects that implement all listed types
  • finally is guaranteed to run even when exit() is called
open as a page

In PHP, how do you wrap a failed payment-provider call in your own domain exception without losing the original cause?

level: middleimportance: must knowfreq 55%

basics

~20 s

Catch the provider client's exception narrowly and throw your own exception class, passing the caught object as the third constructor argument, $previous. Callers depend only on your type, while getPrevious() still returns the provider's original failure for logs and debugging.

open as a page

Inside a PHP namespace, why can a catch (Exception $e) block silently miss a thrown built-in Exception?

level: middleimportance: should knowfreq 30%

basics

~20 s

Inside a namespace an unqualified class name resolves to that namespace, so catch (Exception $e) in namespace App means App\Exception. catch never autoloads or reports a missing class; the clause simply never matches and the exception propagates.

open as a page

In PHP, how do you rethrow a caught exception, and does rethrowing change the file, line and stack trace it reports?

level: middleimportance: should knowfreq 42%

basics

~20 s

PHP has no bare throw; statement — you rethrow with throw $e;. Rethrowing leaves the object untouched: its file, line and trace were recorded when it was created with new, so they still point at the original failure.

open as a page

In PHP, what does a function return when its try block returns one value and its finally block returns another?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The finally block's value. PHP evaluates the try's return expression, runs finally, and a return inside finally replaces the pending value — it also silently discards any exception that was propagating, which is why returning from finally is avoided.

open as a page

In PHP 8.0 and later, how can throw be used inside an expression, and when may a catch block omit its variable?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Since PHP 8.0 throw is an expression, so it can follow ??, ?: or || and form an arrow function's body. PHP 8.0 also made the catch variable optional: catch (CacheMiss) handles the type without binding the object.

open as a page