September 20, 2021·4 min read·engineeringperlcleanupsyntax

Perl 5.35.4 adds experimental scope-exit blocks

Defer registers code when execution reaches it and runs it on leaving the enclosing scope. Its ordering and control-flow limits define what cleanup code can safely express.

Cleanup code often belongs near the operation that makes cleanup necessary, while its execution belongs at the end of a scope. The Perl 5.35.4 release notes introduce experimental defer blocks to express that separation. Reaching the declaration stores a block for later execution. Leaving the containing block invokes it, including exits through exceptions and explicit returns.

This is a runtime registration rule. A defer declaration that execution never reaches does not register anything. The syntax documentation in this release contrasts it with an END block, which the compiler queues regardless of whether normal execution reaches its location. That difference is important for cleanup associated with a successful operation partway through a function.

I prefer cleanup code to identify the scope responsible for it. Perl's compact syntax is useful, but a short function can still have several exits. A scope-exit block gives those exits a common cleanup point without requiring the reader to find a duplicated operation on each branch.

Registration order determines execution order

Multiple deferred blocks in one scope execute in last-in, first-out order. The last declaration reached is the first block invoked during exit. This example makes the ordering visible without introducing a resource library or relying on a platform-specific operation.

use feature 'defer';
no warnings 'experimental::defer';

{
    print "enter\n";
    defer { print "first registered\n"; }
    defer { print "second registered\n"; }
    print "body\n";
}

The body prints before either registered block runs. Scope exit then prints the second registration's message, followed by the first one's message. This ordering allows cleanup for a later acquisition to happen before cleanup for an earlier one. It is a property of the registered blocks, not an instruction to evaluate their bodies when the declarations are first reached.

That temporal distinction needs to remain clear when reading a longer function. A defer statement identifies work to perform later. It does not execute the statements in its body and save their result. Ordinary code between registration and scope exit still runs before the deferred body. A maintainer must consequently read the block as code evaluated during exit.

The declaration can appear where a regular block or another statement is permitted. It is enabled through the named defer feature. The experiment emits a warning when experimental warnings are enabled, and the example suppresses only experimental::defer. That keeps the status of the syntax explicit rather than suggesting the feature is part of the default language accepted by every interpreter.

Leaving the scope has several meanings

The manual includes ordinary fallthrough and function return among the triggering exits. It also includes a thrown exception, whether produced by die directly or propagated from a called function. Loop controls such as next, last, and redo can trigger execution when they leave the relevant scope. A goto that leaves it is included too.

These rules make the containing block the useful unit of explanation. A deferred action belongs to the scope in which it was registered. It is not simply a handler for exceptions, and its execution does not require an error. A normal successful path invokes it as well. This makes it appropriate for expressing actions that should follow either outcome of the body.

The registration rule also prevents cleanup for work that was never reached. A function returning before a defer statement does not enqueue that statement's block. Placing registration after a successful acquisition can therefore associate cleanup with the path that actually acquired the resource. This is a consequence of the documented runtime rule, not a promise about the behaviour of any particular resource API.

Deferred code has limits on outward control flow

A defer body cannot use return to leave its surrounding function. It cannot jump to an outside label or control an enclosing loop. The release adds an error for attempts to jump out of a defer block through such operations. These restrictions keep delayed code from introducing an ordinary outward transfer of control during another exit.

Control flow entirely inside the deferred body remains possible. The manual gives a loop within a defer block whose own last is permitted. The target of the control operation therefore matters. An inner loop can manage its own iteration, while a deferred block cannot use that same keyword to take over a loop surrounding its declaration.

Exceptions are a separate case. A defer body may throw, and its exception propagates to the caller like an ordinary exception. The manual explicitly leaves the result unspecified when deferred code throws while another exception is already causing scope exit, except that the caller receives an exception. That is a substantive limit on what an error-reporting cleanup can promise about preserving the original failure.

I find the explicit restriction useful because cleanup often runs when another operation has already failed. The source documents where exception composition remains undefined. A description that simply calls defer a guaranteed cleanup mechanism would omit that part of its contract.

Alongside the experiment, 5.35.4 adds Unicode 14.0 support and stable tracking of boolean values through assignment. Those are separate changes. The specific contribution of defer is a runtime-registered block with defined scope-exit triggers and reverse registration order, subject to documented limits on how its body can change control flow.