skip to content

Errors & Exceptions

How PHP 8 reports failure: the Throwable tree of Error and Exception, try/catch/finally, and E_* levels with the handlers that convert them. Interviewers probe where the old warning model leaks.

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

explore

questions

17

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 8, what is the difference between the Error and Exception classes, and why does catch (Exception $e) miss a TypeError?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Both implement the Throwable interface but sit on separate branches: Error covers faults PHP itself detects, like TypeError or DivisionByZeroError, while Exception covers failures application and library code reports. catch (Exception) never matches an Error subclass.

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

In PHP, what is the difference between the display_errors and log_errors settings, and how should each be set in production?

level: middleimportance: must knowfreq 62%

basics

~20 s

display_errors writes error messages into the script's output, where visitors see them; log_errors writes them to the error log. Production should run display_errors=Off and log_errors=On, the values php.ini-production ships, so errors are recorded but never shown.

open as a page

How do you turn PHP warnings and notices into exceptions with set_error_handler and ErrorException?

level: middleimportance: must knowfreq 50%

basics

~20 s

Register a callback with set_error_handler() that throws new ErrorException($errstr, 0, $errno, $errfile, $errline). Warnings and notices then become catchable exceptions; the callback should first skip levels excluded by error_reporting(), and fatal or compile-time errors never reach it.

open as a page

In PHP, how do the E_* constants combine into an error_reporting level, and what does E_ALL & ~E_DEPRECATED report?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Each E_* constant is a single bit, so levels combine with | and are removed with & ~. E_ALL & ~E_DEPRECATED, the php.ini-production value, reports every level except engine deprecations; E_ALL, the default since PHP 8.0, reports everything.

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 8, what does the @ error-control operator suppress, and how does it interact with fatal errors and custom error handlers?

level: middleimportance: should knowfreq 36%

basics

~20 s

@ hides the warnings, notices and deprecations one expression raises. Since PHP 8.0 it no longer hides fatal errors; it never stops exceptions; and a custom error handler still runs, seeing error_reporting() return 4437 rather than 0.

open as a page

In PHP, what does set_exception_handler() do, and what happens to the script after the handler has run?

level: middleimportance: should knowfreq 40%

basics

~20 s

set_exception_handler() registers a callback for any Throwable that no catch block handled. PHP unwinds the stack, runs pending finally blocks, calls the handler with the object and then ends the script; execution never resumes at the throw site.

open as a page

In PHP, what separates SPL's LogicException family from its RuntimeException family, and which would you throw for a bad argument versus a missing record?

level: middleimportance: should knowfreq 38%

basics

~20 s

LogicException subclasses signal a programming mistake that should lead to a code fix, such as InvalidArgumentException for a bad argument. RuntimeException subclasses signal conditions only detectable while running, such as OutOfBoundsException or UnexpectedValueException for a missing record or bad data.

open as a page

In PHP 8, when does a function call throw TypeError, ValueError or ArgumentCountError, and how are those three classes related?

level: middleimportance: should knowfreq 45%

basics

~20 s

TypeError means a value has the wrong type; ValueError (PHP 8.0) means the type is right but the value is invalid; ArgumentCountError, a TypeError subclass, means too few arguments, or the wrong count for a built-in.

open as a page

Which PHP 7 warnings became thrown Error exceptions in PHP 8, and what typically breaks when legacy code is upgraded?

level: middleimportance: should knowfreq 40%

basics

~20 s

PHP 8 throws where PHP 7 warned: property writes on null, scalars used as arrays, undefined constants, arithmetic on non-numeric strings, division by zero and invalid built-in arguments. Legacy code that carried on past those warnings now stops.

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

A PHP 8.5 booking site shows a blank white page in production and nothing appears in its error log — how do you track down the cause?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Check the status code, then the serving process's error settings and where its log really goes. Look for handlers that swallow failures, and capture fatal errors — which bypass set_error_handler — with a shutdown function reading error_get_last().

open as a page

A PHP queue worker wraps each job in catch (Exception $e), yet a TypeError from a library call kills the whole worker; why, and how should the job boundary treat each branch?

level: seniorimportance: should knowfreq 35%

basics

~10 s

TypeError extends Error, not Exception, so catch (Exception) lets it escape and the uncaught throwable ends the CLI worker. Catch Throwable at the job boundary, retry only transient Exceptions, and fail Errors as bugs.

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