skip to content

Release at scope exit reports no failure, so for which resources is that unacceptable, and what would you mandate instead?

level: principalimportance: should knowfreq 32%

answer

  1. does the release have an outcome
  2. nothing a caller could do
  3. reportable step inside the scope
  4. explicit complete, destructor abandons
  5. raw acquisition unreachable by policy

basics

~20 s

Silent teardown is fine where release is a local give-back: locks, handles, memory, pooled connections. Anything whose completion can fail and changes what the system believes needs an explicit checked call, with the guard as the rollback backstop.

solid answer

~50 s

I split resources by whether their release has an **outcome**. A lock, a file handle, a buffer or a pooled connection has one: give it back, and there is nothing a caller could do with a failure. Those are safe to leave entirely to teardown. A buffered write that must reach durable storage, a transaction commit, an acknowledgement to a queue — those change what the rest of the system believes, and their failure must reach a caller that can retry or fail the request. For that second class I mandate a two-step type: an explicit `complete()` inside the scope whose error is checked, and a destructor that abandons — rolls back, deletes the partial file, marks the connection broken — and records that it had to. The rule I actually enforce is that acquiring the raw resource is only reachable through the guard type, so no scope can opt out.

go deeper

for a junior

The takeaway: a destructor cannot tell anyone it failed. Anything whose failure you would want to see needs a call you can check.

for a middle

Learn the two-step shape — an explicit completion call that returns a status, and a destructor that undoes the work when that call never happened.

for a senior

Apply the split by hand: sort each resource by whether a caller could act on a release failure, and give only the reportable ones an explicit checked step.

for a principal

Own it as policy with costs attached: hide raw acquisition, default to abandon, count abandons, and say where the policy does not apply rather than forcing it everywhere.

## The split that matters The question is not 'which resources deserve a guard' — all of them do. It is **which resources may have their entire release performed by code that cannot report a failure**. Sort by what a caller could do with an error: | resource | can its release fail meaningfully? | policy | |---|---|---| | mutual-exclusion lock | no; release is a local give-back | guard only, silent teardown | | heap buffer | no | guard only, silent teardown | | file handle opened for reading | no | guard only, silent teardown | | pooled connection | not for the borrower | guard only; mark broken on abandon | | buffered writer with unwritten bytes | yes; the data is lost | two-step: explicit flush, guard discards | | transaction | yes; the work does or does not become visible | two-step: explicit commit, guard rolls back | | unacknowledged message | yes; redelivery or loss follows | two-step: explicit ack, guard abandons | | temporary file destined to be published | yes; a half-written file may be published | two-step: explicit publish, guard deletes | The right-hand column is the whole policy. The first group is genuinely finished by the destructor. The second group has a **reportable step** that must happen inside the scope, where a caller exists, and an **abandon step** that the destructor performs on every path where the reportable one did not run. ## What I mandate 1. **No raw acquisition anywhere.** The operation that takes the lock, opens the handle or begins the transaction is reachable only through the guard type. This is the rule that does the work: it converts leak-on-an-exit-path from a review item into something nobody can write. 2. **Two-step types for the reportable class.** `complete()` returns a checked status and marks the guard done; the destructor does the abandon outcome when it was not called. 3. **Abandon is never silent in observability terms.** A guard destroyed without completion increments a counter and records the scope. Abandoning on an error path is legitimate; abandoning ten thousand times an hour is a defect nobody would otherwise see. 4. **A release must not raise while a failure is already in flight.** The destructor is best-effort and swallows its own errors after recording them, because a second failure travelling the same channel can displace the first or force a harsher outcome. ## The costs I am accepting A principal-level answer says what the policy gives up: - **A two-step API is more to learn** than a single object that 'just works', and a team that forgets the completion call gets correct-but-abandoned behaviour: a rollback where they expected a commit. That failure is loud and safe, which is why the default points that way, but it is still a failure. - **Guard types are a taxed abstraction.** Every resource kind needs one written, reviewed and kept in step with the API underneath. In a small codebase this can cost more than it saves; the policy earns out when the same resource is acquired in dozens of scopes. - **Deterministic release makes hold time visible and therefore arguable.** That is mostly good, but it does push design work onto scope shape — reviewers start arguing about where a block should begin, which is a better argument to be having than the one about a missing release, but it is not free. ## How I would know it is working - The count of hand-written release calls in the codebase trends to zero, and the ones that remain are deliberate early releases with a comment. - Abandon counters are near zero in steady state and spike only when something upstream is genuinely failing. - Incidents stop containing the sentence 'the handle was never released on that path'. ## Where I would not apply it A resource whose lifetime is genuinely longer than any single call — a cache, a pool, a long-lived subscription — is not a scope's to own, and forcing a guard onto it produces either an artificially long scope or a guard whose destruction is disconnected from the work. Those need an owner with a matching lifetime and an explicit shutdown sequence. Scope-bound release is the right default precisely where the resource's useful life and the block's life are the same thing, and stating that boundary is part of owning the policy rather than reciting it.

  • How do you actually stop someone acquiring the raw resource and skipping the guard?
    Make the acquiring operation unreachable outside the guard type — hide it behind the module boundary so the guard's constructor is the only caller. Where the language cannot hide it, a lint rule plus a single reviewed wrapper gets most of the way, but hiding it is what makes the policy self-enforcing.
  • What should the guard do when it is destroyed without an explicit completion?
    Abandon and record. Roll back, delete the partial output, mark the connection broken, and increment a counter naming the scope. Abandoning is correct on an error path, so it must not be an alarm; a rising rate of it is the signal, and without the counter nobody ever sees it.
  • Is the two-step type worth it for a resource acquired in only one place?
    Usually not. The policy earns out when the same resource is acquired across many scopes and the exit paths multiply. For a single call site, an explicit checked release with a plain guard as the backstop is the same guarantee for less machinery, and it is honest to say so.

saying these in an interview costs you the question

  • Leaves a durable flush entirely to a destructor that cannot report failure
  • Makes commit the destructor's default outcome for a transaction guard
  • Claims a guard makes every release safe with no explicit call needed
  • Counts abandoned scopes as alarms rather than as a rate to watch
  • Puts a cache or a pool under a scope guard to satisfy the policy
  • Argues every resource needs a two-step type regardless of call sites