[PHP-DEV] [PRE-RFC] Deprecate returning from a finally block

Hi all,

I’d like to gauge interest in deprecating return inside a finally block, before I request karma to propose it as a full RFC.

A return inside a finally silently discards whatever the try/catch was doing…a pending exception or return value just disappears, with no error or notice.

As far as I know, it’s the only abrupt exit from a finally PHP leaves silent…
break/continue/goto out of one are already compile errors, and a throw auto-chains the discarded exception as $previous. Only return destroys the in-flight state and says nothing.

Looking into the history of finally I found that the author of the original RFC said he added it only because Java allowed it, even though he thought it made “no sense” https://externals.io/message/61670#61678
But Java itself warns about it (javac -Xlint:finally).

I also found that it was the source of a few bugs:

Here’s the list of the occurrences:

  1. ibexa/admin-ui (swallows the exceptions its own docblock declares)
  2. shopware/core (eats thumbnail failures and still reports success)
  3. amphp/http-server (drops exceptions when the stream is already gone)
  4. dvdoug/PHPCoord (deliberate…finally is the method’s return)
  5. dvdoug/PHPCoord (again)
  6. dvdoug/PHPCoord (again)
  7. dvdoug/PHPCoord (again)
  8. spatie/ray (delibrate…any failure just returns false)
  9. cakephp/cakephp (catch consumed the exception…finally just returns array)
  10. hyperf/http-server (catch consumed the exception…finally emits response)
  11. silverstripe/framework (catch consumed the mysqli error)
  12. symfony/flex (try can’t actually throw)

My take here is that the language already handles the analogous cases (continue/break/goto/…etc) natively, so leaving return looks like an inconsistency.

I lean toward deprecating it now with an eye to an error in a future major (following the steps of Tim’s deprecate-return-from-constructor RFC)…though I guess a plain warning would be fine too. The main thing is that it seems worth doing something about.

Is there appetite for this?

Thanks,
Osama

On Thursday, 18 June 2026 at 23:01, Osama Aldemeery <aldemeery@gmail.com> wrote:

Hi all,

I'd like to gauge interest in deprecating `return` inside a `finally` block, before I request karma to propose it as a full RFC.

A `return` inside a `finally` silently discards whatever the `try`/`catch` was doing...a pending exception or return value just disappears, with no error or notice.

As far as I know, it's the only abrupt exit from a `finally` PHP leaves silent...
`break`/`continue`/`goto` out of one are already compile errors, and a `throw` auto-chains the discarded exception as `$previous`. Only `return` destroys the in-flight state and says nothing.

Looking into the history of `finally` I found that the author of the original RFC said he added it only because Java allowed it, even though he thought it made "no sense" [RFC] Supports 'finally' keyword for PHP exceptions - Externals
But Java itself warns about it (`javac -Xlint:finally`).

I also found that it was the source of a few bugs:

- PHP :: Bug #70228 :: Memleak if return in finally block
- PHP :: Bug #72213 :: Finally leaks on nested exceptions
- Heap Buffer Overflow in zval_undefined_cv. · Issue #11028 · php/php-src · GitHub

To see what a change would actually cost, I [implemented the deprecation](Comparing php:master...aldemeery:deprecate-return-in-finally · php/php-src · GitHub) and ran it over the top ~5000 Composer packages. And I found that it's rare...there were only 12 occurrences across 9 packages out of 4992. Of those, 3 look like genuine latent bugs the deprecation caught, and the rest look deliberate.
Here's the list of the occurrences:

- [ibexa/admin-ui (swallows the exceptions its own docblock declares)](admin-ui/src/bundle/Controller/ContentTypeController.php at 2322d5413e2305cd98dbb9f27970e189f67b1b62 · ibexa/admin-ui · GitHub)
- [shopware/core (eats thumbnail failures and still reports success)](core/Content/Media/Thumbnail/ThumbnailService.php at e8079a095432ef1b13d2b063339987caeca6e805 · shopware/core · GitHub)
- [amphp/http-server (drops exceptions when the stream is already gone)](http-server/src/Driver/Http2Driver.php at b306134c9d9fab07ed58b3ac7d0a2e68ad796279 · amphp/http-server · GitHub)
- [dvdoug/PHPCoord (deliberate...finally is the method's return)](PHPCoord/src/Point/CompoundPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub)
- [dvdoug/PHPCoord (again)](PHPCoord/src/Point/GeocentricPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub)
- [dvdoug/PHPCoord (again)](PHPCoord/src/Point/GeographicPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub)
- [dvdoug/PHPCoord (again)](PHPCoord/src/Point/ProjectedPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub)
- [spatie/ray (delibrate...any failure just returns false)](ray/src/Client.php at 2da2079864d4bd82da2fe6a406fc82357179982c · spatie/ray · GitHub)
- [cakephp/cakephp (catch consumed the exception...finally just returns array)](cakephp/src/Database/Query.php at eef91f28de119bee5536905244d2096f752f2920 · cakephp/cakephp · GitHub)
- [hyperf/http-server (catch consumed the exception...finally emits response)](http-server/src/Server.php at 80c52d4d742be46680fdeecc37f9650c533f6e93 · hyperf/http-server · GitHub)
- [silverstripe/framework (catch consumed the mysqli error)](silverstripe-framework/src/ORM/Connect/MySQLiConnector.php at 9c59c428f7fb31e814955496bf3b52fd227ad37a · silverstripe/silverstripe-framework · GitHub)
- [symfony/flex (try can't actually throw)](flex/src/PackageResolver.php at 4a6d98eea3ebc7f68d82810cb682eedca2649e99 · symfony/flex · GitHub)

My take here is that the language already handles the analogous cases (`continue`/`break`/`goto`/...etc) natively, so leaving `return` looks like an inconsistency.

I lean toward deprecating it now with an eye to an error in a future major (following the steps of Tim's [deprecate-return-from-constructor RFC](PHP: rfc:deprecate-return-value-from-construct))…though I guess a plain warning would be fine too. The main thing is that it seems worth doing something about.

Is there appetite for this?

Thanks,
Osama

I would be in favour of this, it should be noted that C# does not permit "exiting" a finally block via control flow statements: [1]

It is a compile-time error for a break, continue, or goto statement to transfer control out of a finally block. When a break, continue, or goto statement occurs in a finally block, the target of the statement shall be within the same finally block, or otherwise a compile-time error occurs.

It is a compile-time error for a return statement to occur in a finally block.

Best regards,
Gina P. Banyard
[1] Statements - C# language specification | Microsoft Learn

Hi Gina, an outright prohibition is essentially the end state I lean toward too.

I think it’s the more consistent fit for PHP…break, continue, and goto out of a finally are already errors here, the same as C#, so making return an error as well would treat them all the same.

Regards,
Osama Aldemeery

On Fri, Jun 19, 2026 at 12:47 PM Gina P. Banyard internals@gpb.moe wrote:

On Thursday, 18 June 2026 at 23:01, Osama Aldemeery <aldemeery@gmail.com> wrote:

Hi all,

I’d like to gauge interest in deprecating return inside a finally block, before I request karma to propose it as a full RFC.

A return inside a finally silently discards whatever the try/catch was doing…a pending exception or return value just disappears, with no error or notice.

As far as I know, it’s the only abrupt exit from a finally PHP leaves silent…
break/continue/goto out of one are already compile errors, and a throw auto-chains the discarded exception as $previous. Only return destroys the in-flight state and says nothing.

Looking into the history of finally I found that the author of the original RFC said he added it only because Java allowed it, even though he thought it made “no sense” https://externals.io/message/61670#61678
But Java itself warns about it (javac -Xlint:finally).

I also found that it was the source of a few bugs:

Here’s the list of the occurrences:

  1. ibexa/admin-ui (swallows the exceptions its own docblock declares)
  2. shopware/core (eats thumbnail failures and still reports success)
  3. amphp/http-server (drops exceptions when the stream is already gone)
  4. dvdoug/PHPCoord (deliberate…finally is the method’s return)
  5. dvdoug/PHPCoord (again)
  6. dvdoug/PHPCoord (again)
  7. dvdoug/PHPCoord (again)
  8. spatie/ray (delibrate…any failure just returns false)
  9. cakephp/cakephp (catch consumed the exception…finally just returns array)
  10. hyperf/http-server (catch consumed the exception…finally emits response)
  11. silverstripe/framework (catch consumed the mysqli error)
  12. symfony/flex (try can’t actually throw)

My take here is that the language already handles the analogous cases (continue/break/goto/…etc) natively, so leaving return looks like an inconsistency.

I lean toward deprecating it now with an eye to an error in a future major (following the steps of Tim’s deprecate-return-from-constructor RFC)…though I guess a plain warning would be fine too. The main thing is that it seems worth doing something about.

Is there appetite for this?

Thanks,
Osama

I would be in favour of this, it should be noted that C# does not permit “exiting” a finally block via control flow statements: [1]

It is a compile-time error for a break, continue, or goto statement to transfer control out of a finally block. When a break, continue, or goto statement occurs in a finally block, the target of the statement shall be within the same finally block, or otherwise a compile-time error occurs.

It is a compile-time error for a return statement to occur in a finally block.

Best regards,

Gina P. Banyard

[1] https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1311-the-try-statement

On Thu, Jun 18, 2026, at 4:49 PM, Osama Aldemeery wrote:

Hi all,

I'd like to gauge interest in deprecating `return` inside a `finally`
block, before I request karma to propose it as a full RFC.

A `return` inside a `finally` silently discards whatever the
`try`/`catch` was doing...a pending exception or return value just
disappears, with no error or notice.

As far as I know, it's the only abrupt exit from a `finally` PHP leaves
silent...
`break`/`continue`/`goto` out of one are already compile errors, and a
`throw` auto-chains the discarded exception as `$previous`. Only
`return` destroys the in-flight state and says nothing.

Looking into the history of `finally` I found that the author of the
original RFC said he added it only because Java allowed it, even though
he thought it made "no sense" [RFC] Supports 'finally' keyword for PHP exceptions - Externals
But Java itself warns about it (`javac -Xlint:finally`).

I also found that it was the source of a few bugs:
PHP :: Bug #70228 :: Memleak if return in finally block
PHP :: Bug #72213 :: Finally leaks on nested exceptions
Heap Buffer Overflow in zval_undefined_cv. · Issue #11028 · php/php-src · GitHub
To see what a change would actually cost, I implemented the deprecation
<https://github.com/php/php-src/compare/master...aldemeery:php-src:deprecate-return-in-finally&gt;
and ran it over the top ~5000 Composer packages. And I found that it's
rare...there were only 12 occurrences across 9 packages out of 4992. Of
those, 3 look like genuine latent bugs the deprecation caught, and the
rest look deliberate.

Here's the list of the occurrences:
1. ibexa/admin-ui (swallows the exceptions its own docblock declares)
<admin-ui/src/bundle/Controller/ContentTypeController.php at 2322d5413e2305cd98dbb9f27970e189f67b1b62 · ibexa/admin-ui · GitHub;
2. shopware/core (eats thumbnail failures and still reports success)
<core/Content/Media/Thumbnail/ThumbnailService.php at e8079a095432ef1b13d2b063339987caeca6e805 · shopware/core · GitHub;
3. amphp/http-server (drops exceptions when the stream is already
gone)
<http-server/src/Driver/Http2Driver.php at b306134c9d9fab07ed58b3ac7d0a2e68ad796279 · amphp/http-server · GitHub;
4. dvdoug/PHPCoord (deliberate...finally is the method's return)
<PHPCoord/src/Point/CompoundPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub;
5. dvdoug/PHPCoord (again)
<PHPCoord/src/Point/GeocentricPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub;
6. dvdoug/PHPCoord (again)
<PHPCoord/src/Point/GeographicPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub;
7. dvdoug/PHPCoord (again)
<PHPCoord/src/Point/ProjectedPoint.php at ced02c4ad44aa4a558f69d53af4834a9d50ab2aa · dvdoug/PHPCoord · GitHub;
8. spatie/ray (delibrate...any failure just returns false)
<ray/src/Client.php at 2da2079864d4bd82da2fe6a406fc82357179982c · spatie/ray · GitHub;
9. cakephp/cakephp (catch consumed the exception...finally just
returns array)
<cakephp/src/Database/Query.php at eef91f28de119bee5536905244d2096f752f2920 · cakephp/cakephp · GitHub;
10. hyperf/http-server (catch consumed the exception...finally emits
response)
<http-server/src/Server.php at 80c52d4d742be46680fdeecc37f9650c533f6e93 · hyperf/http-server · GitHub;
11. silverstripe/framework (catch consumed the mysqli error)
<silverstripe-framework/src/ORM/Connect/MySQLiConnector.php at 9c59c428f7fb31e814955496bf3b52fd227ad37a · silverstripe/silverstripe-framework · GitHub;
12. symfony/flex (try can't actually throw)
<flex/src/PackageResolver.php at 4a6d98eea3ebc7f68d82810cb682eedca2649e99 · symfony/flex · GitHub;
My take here is that the language already handles the analogous cases
(`continue`/`break`/`goto`/...etc) natively, so leaving `return` looks
like an inconsistency.

I lean toward deprecating it now with an eye to an error in a future
major (following the steps of Tim's deprecate-return-from-constructor
RFC
<PHP: rfc:deprecate-return-value-from-construct)...though
I guess a plain warning would be fine too. The main thing is that it
seems worth doing something about.

Is there appetite for this?

Thanks,
Osama

I think this makes sense, but it would definitely need to go through a Deprecation phase. I don't think it needs its own RFC; It can probably go into the omnibus deprecations RFC for this version. (Which should get going very soon.)

--Larry Garfield

I think this makes sense, but it would definitely need to go through a Deprecation phase. I don't think it needs its own RFC; It can probably go into the omnibus deprecations RFC for this version. (Which should get going very soon.)

--Larry Garfield

Hi Larry, agreed on the deprecation phase, that's what I had in mind for 8.6.
On standalone vs omnibus I'm easy either way, still getting a feel for
how that's usually decided, so I'd rather not lock it in yet and see
how the thread develops.

Thanks,
Osama Aldemeery

On Thu, 18 Jun 2026 at 22:58, Osama Aldemeery <aldemeery@gmail.com> wrote:

Hi all,

I'd like to gauge interest in deprecating `return` inside a `finally` block, before I request karma to propose it as a full RFC.

A `return` inside a `finally` silently discards whatever the `try`/`catch` was doing...a pending exception or return value just disappears, with no error or notice.

As far as I know, it's the only abrupt exit from a `finally` PHP leaves silent...
`break`/`continue`/`goto` out of one are already compile errors, and a `throw` auto-chains the discarded exception as `$previous`. Only `return` destroys the in-flight state and says nothing.

Looking into the history of `finally` I found that the author of the original RFC said he added it only because Java allowed it, even though he thought it made "no sense" [RFC] Supports 'finally' keyword for PHP exceptions - Externals
But Java itself warns about it (`javac -Xlint:finally`).

I also found that it was the source of a few bugs:

PHP :: Bug #70228 :: Memleak if return in finally block
PHP :: Bug #72213 :: Finally leaks on nested exceptions
Heap Buffer Overflow in zval_undefined_cv. · Issue #11028 · php/php-src · GitHub

To see what a change would actually cost, I implemented the deprecation and ran it over the top ~5000 Composer packages. And I found that it's rare...there were only 12 occurrences across 9 packages out of 4992. Of those, 3 look like genuine latent bugs the deprecation caught, and the rest look deliberate.

Here's the list of the occurrences:

ibexa/admin-ui (swallows the exceptions its own docblock declares)
shopware/core (eats thumbnail failures and still reports success)
amphp/http-server (drops exceptions when the stream is already gone)
dvdoug/PHPCoord (deliberate...finally is the method's return)
dvdoug/PHPCoord (again)
dvdoug/PHPCoord (again)
dvdoug/PHPCoord (again)
spatie/ray (delibrate...any failure just returns false)
cakephp/cakephp (catch consumed the exception...finally just returns array)
hyperf/http-server (catch consumed the exception...finally emits response)
silverstripe/framework (catch consumed the mysqli error)
symfony/flex (try can't actually throw)

My take here is that the language already handles the analogous cases (`continue`/`break`/`goto`/...etc) natively, so leaving `return` looks like an inconsistency.

I lean toward deprecating it now with an eye to an error in a future major (following the steps of Tim's deprecate-return-from-constructor RFC)...though I guess a plain warning would be fine too. The main thing is that it seems worth doing something about.

Is there appetite for this?

Thanks,
Osama

Hi Osama,

I think for the sake of consistency ( and sanity ), this makes sense.
A deprecation phase in 8.x seems appropriate.

Cheers,
Seifeddine.

On Saturday, 20 June 2026 at 00:45, Osama Aldemeery <aldemeery@gmail.com> wrote:

> I think this makes sense, but it would definitely need to go through a Deprecation phase. I don't think it needs its own RFC; It can probably go into the omnibus deprecations RFC for this version. (Which should get going very soon.)
>
> --Larry Garfield

Hi Larry, agreed on the deprecation phase, that's what I had in mind for 8.6.
On standalone vs omnibus I'm easy either way, still getting a feel for
how that's usually decided, so I'd rather not lock it in yet and see
how the thread develops.

Thanks,
Osama Aldemeery

I'm working on finalizing the 8.6 deprecation list PHP: rfc:deprecations_php_8_6

If you want to chug it in there, feel free to email me (or contact me via another medium) with the content of the proposal.
Ideally DokuWiki format, but I'll work with any format.

Best regards,

Gina P. Banyard

I'm working on finalizing the 8.6 deprecation list PHP: rfc:deprecations_php_8_6

If you want to chug it in there, feel free to email me (or contact me via another medium) with the content of the proposal.
Ideally DokuWiki format, but I'll work with any format.

Best regards,

Gina P. Banyard

Hi Gina, I think that would be great, thank you.
I'll get the proposal content together in DokuWiki and send it over.

Best,
Osama

Hey Osama,

···

On 18.6.2026 23:49:11, Osama Aldemeery wrote:

Hi all,

I’d like to gauge interest in deprecating return inside a finally block, before I request karma to propose it as a full RFC.

A return inside a finally silently discards whatever the try/catch was doing…a pending exception or return value just disappears, with no error or notice.

As far as I know, it’s the only abrupt exit from a finally PHP leaves silent…
break/continue/goto out of one are already compile errors, and a throw auto-chains the discarded exception as $previous. Only return destroys the in-flight state and says nothing.

Looking into the history of finally I found that the author of the original RFC said he added it only because Java allowed it, even though he thought it made “no sense” https://externals.io/message/61670#61678
But Java itself warns about it (javac -Xlint:finally).

I also found that it was the source of a few bugs:

Here’s the list of the occurrences:

  1. ibexa/admin-ui (swallows the exceptions its own docblock declares)
  2. shopware/core (eats thumbnail failures and still reports success)
  3. amphp/http-server (drops exceptions when the stream is already gone)
  4. dvdoug/PHPCoord (deliberate…finally is the method’s return)
  5. dvdoug/PHPCoord (again)
  6. dvdoug/PHPCoord (again)
  7. dvdoug/PHPCoord (again)
  8. spatie/ray (delibrate…any failure just returns false)
  9. cakephp/cakephp (catch consumed the exception…finally just returns array)
  10. hyperf/http-server (catch consumed the exception…finally emits response)
  11. silverstripe/framework (catch consumed the mysqli error)
  12. symfony/flex (try can’t actually throw)

My take here is that the language already handles the analogous cases (continue/break/goto/…etc) natively, so leaving return looks like an inconsistency.

I lean toward deprecating it now with an eye to an error in a future major (following the steps of Tim’s deprecate-return-from-constructor RFC)…though I guess a plain warning would be fine too. The main thing is that it seems worth doing something about.

Is there appetite for this?

Thanks,
Osama

I dislike this.

Yes, it’s a rare occurence. But it’s used legitimately in most places you found. (as you say, only 3 places were genuine issues.)

So, why do you want to remove this? What’s the point? It has some proper usage sites.

We do have well-defined behaviour for return in finally. It works properly (by now) - it’s not like it’s a frequent source of bugs - there were a couple ones when initially introduced 10+ years ago, and a single one more recent.

Also, no, it’s not inconsistent - goto, continue etc. break out of the finally block and continue the functions execution. A return stops function execution as well. These are two different considerations.

Feels a lot like “let’s try changing something for the sake of it”. No thanks.

Thanks,
Bob

On Sat, Jun 20, 2026 at 4:25 PM Bob Weinand <bobwei9@hotmail.com> wrote:

I dislike this.

Yes, it’s a rare occurence. But it’s used legitimately in most places you found. (as you say, only 3 places were genuine issues.)

So, why do you want to remove this? What’s the point? It has some proper usage sites.

We do have well-defined behaviour for return in finally. It works properly (by now) - it’s not like it’s a frequent source of bugs - there were a couple ones when initially introduced 10+ years ago, and a single one more recent.

Also, no, it’s not inconsistent - goto, continue etc. break out of the finally block and continue the functions execution. A return stops function execution as well. These are two different considerations.

Feels a lot like “let’s try changing something for the sake of it”. No thanks.

Thanks,
Bob

Hi Bob,
First, on framing, even though what I am suggesting here is removing it, I deliberately left the door open to a plain warning if people here preferred. The thing is that this is a silent footgun worth addressing…
So I’ll admit I didn’t expect “leave it entirely” to be on the table for something that discards the in-flight state silently.

On the “why” itself, it’s not that the behavior is broken, it works exactly as defined. The problem is what it’s defined to do, a return in finally silently discards a pending exception or return value, with no diagnostic at all. That silence is the whole case. And it’s where the real damage is.
The genuine cases I found are exactly that…a controller that swallows the very exceptions its own docblock declares it throws, a service that reports success after an I/O failure, and an HTTP driver that drops exceptions when a stream is gone. Nothing surfaces any of it, which is why they ship and stay shipped.

So the benefit here is that we close a tiny gap that makes behavior less surprising and more predictable, and the cost of addressing that is close to nothing. It’s in 0.18% of the top 5000 packages, and the deliberate uses don’t even need it, the same behavior can be achieved in other ways (sometimes just one line away…the return just past the finally). So it removes no capability, it just makes the silent cases visible.

On consistency, you’re right…the jump bans are different. But that doesn’t remove the concrete issue = the silent discard. That stays unchanged.

Thanks,
Osama