Re: [RFC] Context Managers

From: Date: Wed, 15 Apr 2026 15:51:28 +0000
Subject: Re: [RFC] Context Managers
References: 1 2 3 4  Groups: php.internals 
Request: Send a blank email to internals+get-130642@lists.php.net to get a copy of this message
On Wed, Apr 15, 2026, at 16:52, Larry Garfield wrote: > On Tue, Apr 14, 2026, at 9:27 AM, Rob Landers wrote: > > On Tue, Apr 14, 2026, at 16:18, Rob Landers wrote: > >> > >> > >> On Tue, Nov 4, 2025, at 21:13, Larry Garfield wrote: > >>> Arnaud and I would like to present another RFC for consideration: Context > >>> Managers. > >>> > >>> https://wiki.php.net/rfc/context-managers > >>> > >>> You'll probably note that is very similar to the recent proposal from Tim and > >>> Seifeddine. Both proposals grew out of casual discussion several months ago; I don't believe > >>> either team was aware that the other was also actively working on such a proposal, so we now have > >>> two. C'est la vie. :-) > >>> > >>> Naturally, Arnaud and I feel that our approach is the better one. In particular, > >>> as Arnaud noted in an earlier reply, __destruct() is unreliable if timing matters. It also does not > >>> allow differentiating between a success or failure exit condition, which for many use cases is > >>> absolutely mandatory (as shown in the examples in the context manager RFC). > >>> > >>> The Context Manager proposal is a near direct port of Python's approach, > >>> which is generally very well thought-out. However, there are a few open questions as listed in the > >>> RFC that we are seeking feedback on. > >>> > >>> Discuss. :-) > >>> > >>> -- > >>> Larry Garfield > >>> larry@garfieldtech.com > >>> > >> > >> Hi Larry/Arnaud, > >> > >> This is a pretty exciting thread and fascinating proposal. That being said, I have a > >> couple of subtle questions that don't seem to be answered in the (very long) thread or the RFC > >> itself -- If I missed it, please let me know: > > >> 1. What happens if a Fiber is suspended in the using block and never resumed? When > >> is the using block released to clean up the context? > > Since it decomposes to a try-catch-finally, it will exit whenever the finally block would have > run if you'd just typed out try-catch-finally yourself. Arnaud checked, and confirmed that > when the fiber is destroyed the using block will exit in a success case (ie, exitContext(null)). > > >> 2. There's still no mention of how this should affect debugging, will we see the > >> "desugared" or "sugared" version? Is that even a concern for the RFC? > > Error messages would see the original code, so "error on line X" would be based on > the original using block. That's the same as any other > desugaring we already do. (PIpes, PFA, constructor promotion, etc.) Debuggers will see the > materialized opcodes, again, the same other desugaring cases. > > >> 3. I will say it is weird to have exitContext return an exception; but what happens > >> if an exception is thrown during exitContext? Why not just have it return void and throw if you need > >> to throw instead of having two paths to the same thing? > > There's a subtle but important difference here: An exception passed through exitContext() > is the original exception from lower in the call stack, and its backtrace will be the original > location of the error. An exception thrown from within exitContext() itself indicates a failure > that the Context Manager is responsible for, usually an error in the exitContext() logic itself. > > Technically a Context Manager can wrap-and-rethrow the exception if it wants, but then it is > "claiming ownership" over it, just like in any other case of wrap-and-rethrow. > > Our expectation is that 90% of the time, "let the exception propagate up unimpeded" > is the desired behavior. This approach makes "return $e" the right thing to do > almost-always, which is nice and simple to remember. > > See the "return values and exception handling" section for a discussion of this in > more detail. As I said in a previous reply, our constraints are different than Python's so we > end up with a different solution. If you have a suggestion for an alternate approach to the > problem, we're happy to listen. > > >> 4. Looking at the desugared form ... I'm a bit confused: if exitContext is > >> called during the finally path and returns an exception, it is just swallowed? But if it is thrown, > >> it won't be? > > The finally path is only reached in case of a successful exit. Therefore there is no exception > to pass in, and thus returning an exception is meaningless. If exitContext() throws a new exception > of its own (which would indicate an error in its own logic), that will just bubble up past the > using block entirely, which is what we want. > > >> 5. That being said, I don't think the RFC shares with us when we should return > >> an exception vs. throw an exception. > > See the "return values and exception handling" section. If something there > isn't clear, let me know and I will try to clarify further. > > >> — Rob > > > > Maybe the desugared version should look more like this? > > > > } catch (\Throwable $e) { > > try { > > $__mgr->exitContext($e); > > } catch (\Throwable $cleanupException) { > > throw new ContextManagerException( > > $cleanupException->getMessage(), > > previous: $e > > ); > > } > > throw $e; > > } > > > > I'm not sure I see a reason to force any new exceptions to be only of the > ContextManagerException type. If there's a TypeError inside exitContext() or something, > I'd expect that to be propagated as a TypeError. > > --Larry Garfield Thanks Larry, this clears things up for me. The example I had in mind is distinguishing between errors in a transaction: class DatabaseTransaction implements ContextManager { public function __construct(private PDO $connection) {} public function enterContext(): PDO { $this->connection->beginTransaction(); return $this->connection; } public function exitContext(?\Throwable $e = null): ?\Throwable { if ($e) { $this->connection->rollback(); // PDO throws: server has gone away } else { $this->connection->commit(); } return $e; } } // Application code: using ($db->transaction() => $conn) { $conn->execute('INSERT INTO orders ...'); // throws ValidationException } The application will see the PDOException about the server going away, but the original ValidationException that caused the rollback attempt is lost entirely. My point with my suggestion wasn't to hide an exception behind a specific type, but to use exception chaining native to PHP to preserve both independent failures in a way that an application can understand what actually happened. In other words, as a developer, all I'd see is a rollback failed due to a server disconnection. I'd be missing what caused the rollback in the first place. I'd be fine if the desugared catch path simply attached the original exception as $previous on whatever escapes exitContext(), so the root cause isn't lost. That's a one-line change in the engine: just set the previous property at the bottom of the cleanup exception's chain before rethrowing. — Rob

« previous php.internals (#130642) next »