When a scope exits because an error is unwinding, what must a guard's release do differently from a normal return?
answer
- teardown runs, outcome differs
- guard cannot see the error
- abandon unless told completed
- roll back is the safe default
- never raise while already unwinding
basics
~20 sIt must assume the work was abandoned, not finished: a transaction guard rolls back, a temporary-file guard deletes, a partial output is discarded. It must also not raise a failure of its own, because a second failure during unwinding has nowhere to go.
solid answer
~50 sTeardown itself is identical — the scope destroys its guards either way — so the difference lives inside the guard. A guard cannot see an error, but it can see whether the scope told it the work **succeeded**. So a well-built guard defaults to the abandon outcome and only performs the success outcome if an explicit completion call flipped a flag earlier in the scope: roll back unless committed, delete the partial file unless it was published, discard the buffer unless it was flushed. The second rule is about failures: a release that raises its own error while an error is already propagating leaves two failures and one channel, so the original can be lost or the process brought down. Releases that can genuinely fail belong in an explicit checked call, with the guard as the backstop.
go deeper
The key fact: an error leaving a scope still releases everything the scope held. Teardown is not skipped just because something went wrong.
Explain the two-state guard — it defaults to the abandon outcome and only does the success outcome when an explicit completion call told it the work finished.
Show the production judgment: decide which releases can genuinely fail, give those an explicit checked call, and keep the destructor best-effort so it never fails while a failure is in flight.
Own the default. A guard that commits on destruction converts an abandoned scope into published bad data; mandate abandon-by-default and make un-completed destruction observable.
## Teardown does not know why the scope ended The scope's teardown is the same code on both paths — it destroys the guards it owns, in reverse order. What differs is the **outcome the guard should choose**, and a destructor is not handed the reason the scope ended. The standard design therefore inverts the question: instead of asking 'did an error happen?', the guard asks 'was I told the work completed?'. That gives the two-state guard: 1. Construction acquires the resource and records `completed = false`. 2. An explicit call inside the scope — commit, publish, flush, hand over — does the work whose failure matters, checks it, and sets `completed = true`. 3. The destructor performs the **abandon** outcome unless `completed` is set. The abandon outcome is the safe default because it is the one that is correct when the scope ended unexpectedly, and unexpected exits are exactly the ones nobody wrote code for. | resource | success outcome (explicit) | abandon outcome (destructor default) | |---|---|---| | transaction | commit, error checked by the caller | roll back | | temporary export file | rename into place as the published file | delete the partial file | | buffered writer | flush and confirm durability | discard the unwritten bytes | | lock | nothing — release is the only outcome | release | | pooled connection | return it to the pool as reusable | return it marked broken, or close it | The lock row matters: some resources have exactly one outcome, and for those the plain guard with no completion call is complete and correct. ## The second rule: do not fail while failing A destructor has no caller to return a status to. During unwinding there is already a failure in flight, and a release that raises a second one leaves two failures and one channel. Designs differ in what happens then — the original failure can be replaced by the newer one, the newer one can be swallowed, or the whole process can be brought down — and none of those is a good outcome to design for. The rule that follows is blunt: - **A release whose failure changes correctness must not be left to teardown alone.** Flushing bytes that must reach durable storage, committing, acknowledging receipt: do it explicitly, inside the scope, where the failure has a caller. - **Teardown does the best-effort part**: close the handle, unlock, discard, roll back — and swallows or records its own errors rather than propagating them. This is not an argument against scope-bound release; it is what makes it complete. The explicit call handles the reportable outcome, the destructor handles every path where that call never happened. ## Partial construction, again Unwinding out of the middle of a scope's own setup is the same rule applied one level down. The guards already fully constructed are destroyed in reverse; the one whose constructor was in flight is not, since it never became an object with an obligation. Practically: a constructor that acquires two things should hold each in its own member guard, so that a failure of the second releases the first automatically rather than requiring a hand-written cleanup inside the constructor. ## What an interviewer is listening for - That teardown **runs** on the unwinding path — many candidates assume an error path skips it, which is the whole reason manual release at the bottom of a function is a defect. - That the guard defaults to **abandon**, not to commit. A guard that commits on destruction turns an abandoned scope into published bad data, which is far worse than a leak. - That there is a distinction between a release that can fail meaningfully and one that cannot, and that the first gets an explicit checked call. - That the guard can record the anomaly: being destroyed without completion is legitimate on an error path, but a guard that logs or counts it turns a silent abandon into an observable one. Said in one line: the scope guarantees the release happens; the guard decides which release it is, and the safe default on the path nobody planned for is to undo rather than to finish.
- How does the guard learn that the work succeeded, if it cannot see the error?Through an explicit completion call earlier in the scope that does the reportable part of the release and sets a flag on the guard. The destructor then reads the flag: set means the success path already happened, unset means abandon. The reason for the exit never has to be inspected.
- Why can teardown not simply return its failure to the caller?There is no caller to return to — teardown runs on an edge out of the block, not as an ordinary call, and on the unwinding path a failure is already travelling that channel. A second one would either displace the first or force a harsher outcome, so teardown is designed to be best-effort and silent.
- A constructor acquires two resources and the second acquisition fails. What is the clean structure?Hold each acquisition in its own member guard. Then the failure of the second destroys the first automatically as the partially built object unwinds, and no cleanup code is written inside the constructor at all — which is the same rule applied one level down.
saying these in an interview costs you the question
- Assumes teardown is skipped when an error unwinds out of the scope
- Designs a transaction guard that commits on destruction by default
- Lets a release raise its own error while a failure is already propagating
- Leaves a flush whose failure loses data entirely to the destructor
- Thinks the destructor can inspect why the scope ended
- Writes hand-rolled cleanup inside a constructor instead of member guards