In PHP, how do try, catch and finally blocks work together, and in what order are multiple catch blocks tried?
answer
- try needs a catch or a finally
- source order, first match wins
- match means instanceof the named type
- one catch, several types joined by |
- finally runs on the way out
basics
~20 sCode 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 sA `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
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
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.
Explain first-match versus best-match ordering, how | lists several types, and walk through each way a try can end and whether finally runs.
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.
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