A handler is stored for later that captures a reference to a row created inside the registering function. Why is that rejected?
answer
- capture lengthens the window
- a stored callback runs whenever
- returned, stored, or closed over
- invoked now is fine, deferred is not
- give it ownership or give it a key
basics
~20 sStoring the handler gives the captured reference an unbounded window: it may run at any later point, including after the registering function has returned and released the row. A borrow may only be used inside a window contained in the owner's lifetime.
solid answer
~40 sA captured reference is still a borrow, and the capture does not shorten its window — it lengthens it to whenever the handler might run. Once the handler is stored in a registry, a queue, or a later phase of the pipeline, that moment is unbounded, so the containment the discipline requires cannot be established. The contrast worth stating is with a callback **invoked during the call**: there the whole use window sits inside the borrow's window, so borrowing is fine. The escape routes are the same three every time — returned, stored in something longer-lived, or captured by something stored. The remedies are to hand the handler ownership of what it needs, hand it an owned copy, or hand it a key it re-resolves against a table supplied when it actually runs.
code
pseudocode · 5 linesfunction process(table):
row = table.row_at(0) // a borrow into the table
with_row(row, function(r): print(r.name)) // invoked now: uses stay inside the window
on_save(function(): print(row.name)) // stored: may run after row's window ends
// fix: on_save(function(t): print(t.row_at(0).name)) -- borrow taken at call timego deeper
Recall the difference between a callback that runs right away and one that is stored and run later. Only the first may safely hold a reference to something local.
Explain that capturing a reference keeps it a borrow and stretches its use window to whenever the handler may fire, which is why the containment check fails.
Name all three escape routes, including capture, and pick a remedy for the situation at hand — ownership, an owned copy, or a key resolved at invocation time — and justify the choice.
Set the convention: deferred work owns its inputs. A type that captures references spreads its constraint through every structure that holds it, and that spread is far more expensive to undo than to prevent.
## Capture is not a shortcut past the rule When a function value captures a reference, the reference does not become something else. The captured borrow carries the same requirement it always had: every use must fall inside a window contained in the owner's lifetime. What capture changes is **who decides when the uses happen** — and once the function value is stored, the answer is "whoever calls it, whenever that is". That is why the same reference is acceptable in one callback and rejected in another: - **Invoked now.** The callback runs inside the call that created it, so the entire set of uses provably precedes the point where the registering function returns. Containment holds, and a borrow is the right thing to pass. - **Stored for later.** The callback outlives the call that built it. There is no point in the program after which the discipline can say "the borrow is definitely no longer used", so the check fails. ## The three escape routes Every instance of this defect is one of three shapes, and naming them is what an interviewer is listening for: 1. **Returned.** The reference is the result, and the value it points into is local. 2. **Stored.** The reference is written into a structure that outlives the frame — a cache, a registry, a list of pending work. 3. **Captured.** The reference is closed over by a function value that is itself returned or stored, which is route 1 or 2 wearing a disguise. Route 3 is the one that gets past review, because the reference never appears in a signature. The capture is implicit, so the code reads as if only a function is being stored. ## Fixing it: give the handler something it can keep | Approach | What the handler holds | Cost | When it fits | |---|---|---|---| | Hand it ownership | The value itself, moved in | The registering function can no longer use it | The value exists only to serve the handler | | Hand it an owned copy | An independent copy of what it needs | Bytes, plus the copy drifts from the source | The needed part is small and a snapshot is correct | | Hand it a key | An index or identifier, resolved when it runs | One lookup, and the row may be gone by then | The source of truth must stay live | | Scope the handler | Nothing; it runs before the frame ends | It cannot be deferred at all | The work is genuinely synchronous | The key approach is the one that scales, because it moves the question from "is this reference still valid?" (unanswerable at a distance) to "does this row still exist?" (answerable, by the owner, at the moment it matters). It also forces the honest conversation the borrow was hiding: what *should* happen if the row was deleted before the handler ran? ## What this reveals about lifetimes as an interface A deferred handler that borrows is really an API that has promised its caller something it cannot honour. Two design points follow: - **A stored callback should not accept borrowed context at registration.** If it needs context, take it as a parameter supplied at invocation time, when a fresh borrow can be proved valid. - **A type that captures references is contagious.** Every structure that holds it inherits the constraint, and it spreads outward through your API until it reaches something that cannot satisfy it. Deciding early that deferred work owns its inputs keeps that constraint from becoming a refactor. Ecosystems differ in how loudly the mistake announces itself: some refuse to compile the registration, some fail at runtime when the handler eventually fires, and some quietly read whatever now occupies the released storage. The design answer is identical in all three, which is a good reason to adopt it regardless of what enforces it. ## Reviewing for it The reference never appears in a signature, so grep for the *shape* rather than the symptom: - A registration call taking a function value built in the same block as the data it reads. - A queue, timer, retry wrapper or event table whose entries were constructed inside a request-scoped frame. - A handler whose body mentions a name that is not one of its own parameters — the giveaway that something was captured. In each case ask one question: *what is the latest moment this can run, and is the value it reads certainly alive then?* If the answer needs a sentence about how the system is usually scheduled, the borrow is wrong, because scheduling is not a lifetime argument. Rewrite the handler to take its context as a parameter at invocation time and the question stops being askable — which is the real reason the convention is worth having before anyone hits the bug.
- Why is a callback that is invoked during the call allowed to take a borrow?Because every use it makes provably happens before the call returns, so the whole use window sits inside the borrow's window. The discipline is not suspicious of callbacks as such; it is suspicious of uses it cannot bound in time.
- The handler is given a key instead of a reference. What new question does that raise?What it should do when the row is gone. A key can fail to resolve, which makes deletion an explicit case the handler must handle, rather than an invisible dangling read. That is the same design question the borrow was quietly deferring.
saying these in an interview costs you the question
- Thinks capturing a reference makes a copy of the value
- Says a stored callback is safe because it runs soon afterwards
- Believes the capture extends the captured value's lifetime
- Cannot distinguish an immediately invoked callback from a stored one
- Treats it as a threading problem rather than a lifetime problem