In PHP, how do you rethrow a caught exception, and does rethrowing change the file, line and stack trace it reports?
answer
- no bare throw; form
- throw $e; with the same object
- trace captured at new, not at throw
- getLine() points at creation
- catch narrowly, let the rest bubble
basics
~20 sPHP 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.
solid answer
~40 sInside a `catch`, `throw $e;` sends the same object onward; PHP has no bare `throw;` form. Rethrowing resets nothing, because the engine records `file`, `line` and the backtrace when the exception object is **constructed**, not when it is thrown — so `getFile()`, `getLine()` and `getTraceAsString()` still point at the original failure. The flip side is that an exception built inside a factory method reports the factory's line. Rethrow when you need to act and still fail: log with extra context, undo something, or inspect a property and decide the case is not yours. The larger problem is usually the catch itself — `catch (\Throwable $e)` around a whole operation also traps `TypeError` and other programming bugs, so catch the narrowest type you can actually handle and let everything else propagate.
code
php · 15 lines<?php
function fail(): never
{
throw new RuntimeException('disk full'); // line 4: file, line and trace are recorded here
}
try {
try {
fail();
} catch (RuntimeException $e) {
throw $e; // line 11: rethrow does not touch the recorded origin
}
} catch (RuntimeException $e) {
echo $e->getLine(), PHP_EOL; // 4
}go deeper
Recall that throw $e; rethrows and that PHP has no bare throw; statement.
Explain that file, line and trace are captured when the object is constructed, and choose between rethrowing, wrapping and handling for a given catch.
Spot over-broad Throwable catches and log-and-rethrow at every layer in review, and show how a new wrapper with $previous records where a failure was translated.
Set the team's policy on where failures are logged and handled so traces stay intact and each incident appears once in the logs.
## Rethrowing syntax To pass a caught exception on, throw the same variable again: ```php try { $this->ledger->record($entry); } catch (DuplicateEntry $e) { $this->metrics->increment('ledger.duplicate'); throw $e; } ``` Some languages offer a bare `throw;` that means "rethrow the current exception". **PHP has no such form** — `throw` always needs an expression, so a bare `throw;` is a parse error. If the `catch` clause omitted its variable, there is no way to rethrow that object; name the variable whenever you might rethrow. A rethrow from `catch` still passes through the `finally` block of the same `try` before the exception continues upward. ## Where file, line and trace come from An exception object records its origin **at construction time**. When `new SomeException(...)` runs, the engine fills in the file, the line and a backtrace of the call stack at that moment. | Property | Set when | Changed by `throw $e;` later? | |---|---|---| | `getFile()` | the object is created | No | | `getLine()` | the object is created | No | | `getTrace()` / `getTraceAsString()` | the object is created | No | | `getMessage()`, `getCode()` | passed to the constructor | No | Consequences worth knowing: - a rethrown exception keeps pointing at the **original** failure, which is usually what you want; - an exception created in a helper such as `self::notFound($id)` reports the helper's line, and the trace shows the helper as the most recent frame; - an exception stored in a variable and thrown much later still reports where it was built; - there is no public method to reset those values, and exception objects cannot be cloned — `Exception::__clone()` is private. If you want the stack to show **where the failure was translated**, create a *new* exception and link the caught one as `$previous`. The new object gets a fresh file, line and trace, and the chain keeps the old ones. ## Rethrow, wrap or swallow | Option | Syntax | When it fits | |---|---|---| | Rethrow | `throw $e;` | You need a side effect (metric, log with context, cleanup) but the failure is not yours to resolve | | Wrap | `throw new DomainFailure('...', previous: $e);` | The caller should see your vocabulary, not the lower layer's | | Handle | no throw | You can genuinely recover: a fallback value, a retry that succeeded, a user-facing validation message | | Swallow | empty `catch` | Almost never; at minimum leave a comment saying why the failure is irrelevant | ## Over-broad catches The commonest catch-related defect is catching too much: 1. `catch (\Throwable $e)` also traps every `Error`: `TypeError`, `ValueError`, `ArgumentCountError`. Those are programming bugs, and turning them into "operation failed, logged" hides them. 2. `catch (\Exception $e)` is narrower, but still catches failures the block has no plan for, such as a database outage inside an HTTP client wrapper. 3. A `catch` that logs and then **does not rethrow** turns a failure into a silent success for the caller, who carries on with missing data. The discipline: catch the most specific type you can do something about; rethrow or wrap everything you inspected but cannot resolve; leave application-wide `\Throwable` handling to one place at the top of the program. | Instead of | Prefer | Because | |---|---|---| | `catch (\Throwable $e)` around business logic | the specific exception the called code documents | `Error` subclasses signal bugs, not conditions to handle | | `catch (\Exception $e)` around one client call | that client's own exception type or interface | an unrelated failure is not relabelled as a remote failure | | one broad `catch`, then `if ($e instanceof ...)` | several `catch` clauses, or `\|` | the language already dispatches on type | The one place where catching `\Throwable` is right is the outermost boundary — a front controller, a worker loop, a console command runner — whose job is to record any failure and keep the process's own contract. ## Logging and rethrowing together Logging in a `catch` and then rethrowing is legitimate when the log line adds context the outer layers do not have. Done at every layer, it writes the same failure to the log many times. Pick one of: add context here and rethrow without logging, or log once where the exception is finally handled.
- If you want the stack trace to show where an exception was translated, not only where it started, what do you do?Create a new exception at the translation point and pass the caught one as `$previous`. The new object records its own file, line and trace when it is constructed, and the chain still carries the original's. Rethrowing the old object with `throw $e;` cannot do this, because nothing resets the recorded origin.
- Can you rethrow from a catch block written as catch (DuplicateEntry) without a variable?No. A variable-less catch never binds the object, and PHP has no bare `throw;` to rethrow the current exception. If the block might rethrow, give it a variable and use `throw $e;`.
saying these in an interview costs you the question
- A bare throw; rethrows the current exception, as in other languages
- Rethrowing resets the stack trace to the catch block's line
- catch (\Throwable $e) is a safe default for any risky block
- Logging in every catch layer and rethrowing makes failures easier to trace
- An exception's line is where the throw statement is, not where new ran