skip to content

A section block opens a scratch file and then exits by raising an error: what does block-bound release guarantee about that file?

level: seniorimportance: should knowfreq 44%

answer

  1. every exit, not just the last line
  2. a trailing call is skipped
  3. bind release to the block, not to a statement
  4. the acquire-register window is unprotected
  5. escaped ownership must not be released here

basics

~20 s

Nothing, unless the release was bound to the block's exit rather than written as a trailing statement. A release bound to block exit runs on every path out, the raising one included; a trailing call is simply never reached.

solid answer

~40 s

The guarantee depends entirely on which shape was used. If the release is just the block's last statement, it is reached only by falling through, so an early return or a propagating error skips it and the file stays open. If the release is **bound to the block's exit** - registered when the resource is acquired and run on every way out - then the raising path unwinds through it and the file is closed. Even then the guarantee has edges worth naming: the window between acquiring and registering is unprotected, a release that itself fails during unwinding can mask or compound the original error, and if ownership of the file escaped the block, firing the block's release leaves the outliving owner holding something already closed.

code

pseudocode · 4 lines
pseudocode
function render_section(section)
    declare handle = open_scratch()
    write(handle, section)   // raises on a malformed section
    close(handle)            // never reached on the raising path

go deeper

for a junior

Recognise that a cleanup call written as the last line of a block is reached only when control falls through to it, so an error on an earlier line skips it.

for a middle

Enumerate the exits a block has - fall-through, early return, loop exit, propagating error - and explain why registering the cleanup with the block covers all of them.

for a senior

Name the edges you have actually been burned by: the gap between acquiring and registering, a value whose ownership escaped, and a cleanup failure that replaced the real error.

for a principal

Decide the ownership convention for the codebase - who releases when a value escapes its block - so that leaks and double releases are design errors rather than review catches.

## What block-bound release actually ties together Block structure gives a program a natural bracket: control enters a block, and eventually leaves it. **Block-bound release** uses that bracket as the release point for a resource - the block's exit, rather than a particular statement, is what runs the cleanup. It is the deliberate use of a *scope* boundary to manage a *lifetime*, which is why it belongs to this subject at all. The distinction that decides the answer is between two shapes that look similar on the page: - **Release as a trailing statement.** Acquire near the top, release near the bottom. The release is reached only when control falls through to it. - **Release bound to the exit.** At the moment of acquisition, register the cleanup with the block itself. Leaving the block by any route runs it. ## Every way a block can be left A candidate who only pictures the fall-through path will get this wrong. A block can be left by: 1. **Falling through** the last statement - the only path a trailing release covers. 2. **Returning early** from somewhere in the middle. 3. **Breaking or continuing** out of an enclosing loop construct. 4. **Propagating an error** raised by any statement in the block, including the acquisition's own next line. 5. **Being abandoned** by a shutdown or a cancellation that unwinds the block. | | trailing release statement | release bound to block exit | |---|---|---| | normal fall-through | runs | runs | | early return | skipped | runs | | propagating error | skipped | runs | | loop exit out of the block | skipped | runs | | an edit inserting a return above it | silently skipped | still runs | That last row is the maintenance argument, and it is stronger than the correctness argument: the trailing shape can be correct on the day it is written and be broken by an edit that adds an early exit and never looks at the bottom of the block. ## What the binding still does not cover Binding release to the exit is the right default, and it is not a total guarantee. Four edges are worth naming out loud: - **The acquire-then-register window.** If the resource is acquired and something fails before the cleanup is registered, the resource is live and nothing owns it. Acquire and register as one indivisible step, never as two statements a later edit can separate. - **Escaped ownership.** If the value was stored into something that outlives the block, releasing at block exit hands the outliving owner a closed resource, and its next use fails. When ownership escapes, the release obligation must escape with it and the block has to give up its claim explicitly. - **A release that itself fails.** Cleanup can raise, and it raises while another error is already travelling outward. Left unhandled, it can replace the original error with a less informative one, which is why the failure being reported is not always the failure that happened. - **Resources acquired inside the release path.** Cleanup that allocates in order to clean up can fail for the same reason the block did, and the second failure has no bracket of its own. ## Why the block is nonetheless the right bracket The alternative - release at the last use - requires every reader and every future editor to know where the last use is, and that changes with every edit. The block boundary is visible, is already the unit the reader is tracking for names, and survives the insertion of new statements. It also gives a reviewer a simple check: for each acquisition, is there a bracket, and does the resource escape it? Two questions, answerable by reading one block. The cost is that the lifetime is now as long as the block, which can be longer than necessary. A resource held across a slow stretch of unrelated work is held for no reason. The remedy is to narrow the block - introduce an inner block around the acquisition and its uses - rather than to abandon the bracket, because narrowing keeps the guarantee and shortens the hold. ## What a strong answer sounds like Say the shape first, then the guarantee, then the edges: a trailing release promises nothing on an error exit; a release bound to the block's exit promises to run on every exit; and the promise stops at the acquire-register window, at escaped ownership, and at a cleanup that fails during unwinding. Finishing with a sentence about how you would review it - one bracket per acquisition, and an explicit answer to whether the value escapes - is what separates a production answer from a textbook one.

  • Which window stays unprotected even when release is bound to the block's exit?
    The window between acquiring the resource and registering its release. If the acquisition succeeds and the registration does not run - a failure in between, or an edit that separated the two lines - the resource is live and nothing owns it. Acquire and register as one step so no statement can be inserted between them.
  • What goes wrong when the resource's ownership escapes the block?
    The block's release fires on something another object still holds, so the outliving owner's next use of it fails on an already-released resource. Once ownership escapes, the obligation has to escape with it: the new owner releases, and the block gives up its claim explicitly instead of both running.

saying these in an interview costs you the question

  • Assumes statements after the failing line still run on the error path
  • Believes the name leaving scope is itself the release
  • Thinks one release at the end of the routine covers every exit
  • Says block-bound release is still safe after ownership escaped the block
  • Ignores a cleanup that fails while an error is already propagating