skip to content

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%

answer

  1. return value is computed before finally
  2. the later return wins
  3. a pending exception is discarded too
  4. changing the variable in finally is too late
  5. exception in finally chains the earlier one

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.

solid answer

~50 s

When `try` reaches `return $x;`, PHP evaluates `$x` at once, holds that value, and runs `finally` before leaving the function. If `finally` executes its own `return`, that value replaces the held one: `try { return 'try'; } finally { return 'finally'; }` returns `'finally'`. Worse, a `return` in `finally` also cancels a pending exception — if `try` or a `catch` threw, the caller receives an ordinary return value and the failure vanishes without a trace. Conversely, reassigning `$x` inside `finally` does not change the result, because the value was already taken; mutating an object it refers to is visible, since the held value is the same object handle. If `finally` throws while another exception is pending, the new one propagates and the earlier one is attached at the end of its `getPrevious()` chain. Keep `finally` for cleanup only.

code

php · 11 lines
php
<?php
function charge(): string
{
    try {
        throw new RuntimeException('provider timeout');
    } finally {
        return 'ok';
    }
}

var_dump(charge()); // string(2) "ok" - the RuntimeException never reaches the caller

go deeper

for a junior

Recall that finally runs even after return, and that a return inside finally replaces the value from try.

for a middle

Explain that the return expression is evaluated before finally, why reassigning a scalar there does nothing, and why mutating an object does.

for a senior

Recognise that a return in finally swallows in-flight exceptions, explain the previous-chain behaviour when finally throws, and guard cleanup so it cannot mask failures.

for a principal

Make finally-as-cleanup-only a reviewed convention, and decide where cleanup failures are logged so the original incident is never masked.

## Order of operations `finally` sits between a `return` and the function actually returning. For `return $total;` inside a `try`: 1. PHP evaluates the expression `$total` **immediately** and stores the result as the pending return value. 2. It runs the `finally` block. 3. If `finally` finishes normally, the function returns the stored value. 4. If `finally` executes its own `return`, the stored value is thrown away and the new one is returned. The same holds for a `return` inside a `catch` block: the value is computed, `finally` runs, and a `return` in `finally` wins. ## Every combination in one table | `try` / `catch` does | `finally` does | Caller sees | |---|---|---| | `return 'a'` | nothing special | `'a'` | | `return 'a'` | `return 'b'` | `'b'` | | throws `E1` | nothing special | `E1` propagates | | throws `E1` | `return 'b'` | `'b'` — **`E1` is discarded** | | throws `E1` | throws `E2` | `E2`, with `E1` at the end of `E2`'s `getPrevious()` chain | | `return 'a'` | throws `E2` | `E2`; the pending `'a'` is lost | The fourth row is the dangerous one. A `return` in `finally` does not merely replace a value; it **cancels the in-flight exception**. A timeout from a payment call, a failed write, a rethrow from a `catch` — all disappear, and the caller carries on as if the operation succeeded. There is no warning or log entry. ## What finally cannot change Because the pending value is computed before `finally` runs: - reassigning the returned variable in `finally` has **no effect** on the result — `return $n;` followed by `$n = 99;` in `finally` still returns the old `$n`; - the same applies to arrays: the held array is a separate value, so modifying the local variable afterwards does not alter it; - **objects are different**: the held value is an object handle, so `$result->status = 'closed'` inside `finally` is visible to the caller, since both refer to the same object. ## When finally throws If `finally` throws while another exception is already propagating, PHP does not lose the first one entirely: the exception from `finally` propagates, and the earlier exception is attached at the **end** of its `getPrevious()` chain. The PHP manual demonstrates this with a four-exception example, where the chain reads finally's exception, its own previous, then the try's exception and its previous. A handler that walks `getPrevious()` still sees both failures, but the type the caller catches is the one from `finally` — often a cleanup error unrelated to the real problem. ## Why PHP allows return in finally at all Other jumps out of `finally` are refused at compile time: a `break`, `continue` or `goto` that would leave the block fails with "jump out of a finally block is disallowed". `return` is different because it ends the whole function, and the language defines what it means — the last value wins. The behaviour is deliberate and covered by php-src's tests, so such code is valid; it is simply rarely what the author intended. A quick self-test when reviewing a `finally` block: - Could anything here throw? If so, is it guarded? - Does anything here `return`? If so, what happens to an exception from `try`? - Does anything here reassign a variable that `try` returned, expecting the caller to see the change? ## Production guidance - Treat `finally` as **cleanup only**: release a lock, close a handle, restore a setting. - Do not `return` or deliberately `throw` from `finally`. - Guard cleanup that can itself fail — a `fclose()` on a broken stream, an unlock against a lost connection — with its own inner `try`/`catch`, so a cleanup failure does not mask the original exception. - When a function must return a value computed during cleanup, compute it after the `try` statement instead, where normal control flow applies. - In code review, any `return` inside `finally` deserves a question; it is almost always a bug waiting for the first exception.

  • A catch block rethrows with throw $e; and the finally block contains return true;. What does the caller receive?
    `true`. The rethrow starts propagation, `finally` runs on the way out, and its `return` replaces the in-flight exception with an ordinary value. The caller never sees the failure, and nothing is logged. That is why a `return` in `finally` is treated as a defect in review.
  • How do you keep a failing cleanup step in finally from hiding the original exception?
    Wrap the cleanup in its own `try`/`catch` inside `finally` and log or ignore the cleanup failure there. Otherwise an exception thrown from `finally` becomes the one the caller catches, with the real failure buried at the end of its `getPrevious()` chain.

A courier has already sealed your parcel when the office manager, doing the closing checklist, swaps it for a different box at the door. The recipient gets the manager's box — and a note saying the original parcel was damaged gets thrown away with it.

saying these in an interview costs you the question

  • The first return statement always wins, so finally cannot change the result
  • A return in finally still lets a pending exception reach the caller
  • Setting the returned variable inside finally changes what the function returns
  • When finally throws, the original exception is simply lost
  • PHP refuses to compile a return statement inside finally