skip to content

A closure captured a copy of a variable holding a mutable record — why does it still see a field written afterwards?

level: middleimportance: should knowfreq 44%

answer

  1. two different changes, one word
  2. the copy is of the variable
  3. reassignment versus writing a field
  4. capture freezes the name, not the data
  5. snapshot the values you actually need

basics

~20 s

Because the copy is a copy of what the variable held — a handle to the record, not the record. Capture semantics governs reassignment of the name; it never freezes whatever the name points at.

solid answer

~40 s

Capture by value copies the **contents of the variable**, and nothing more. If those contents are a handle to a record, the closure and the enclosing scope end up holding two handles to one record, so a field written through either is visible through the other. The only change the two capture models actually disagree about is **reassigning the variable itself** to refer to something else: a copy keeps pointing at the original record, a captured binding follows the reassignment. To get a genuine snapshot, capture a value that cannot be written through — copy the specific fields you need into fresh locals at capture time, or build a finished value and capture that.

code

pseudocode · 9 lines
pseudocode
order = record(total: 10)

showTotal = function() { report(order.total) }   // captures the variable's contents

order.total = 99        // mutation: the record changed, the variable did not
showTotal()             // 99 under BOTH capture models

order = record(total: 0)  // reassignment: the variable now holds something else
showTotal()             // copied value -> 99 ; captured binding -> 0

go deeper

for a junior

Separate the two events: assigning the variable something else, versus writing a field inside what it already refers to. Capture rules are about the first.

for a middle

Explain that copying a variable copies a handle, so both sides reach one record, and identify the single case where the two capture models actually differ.

for a senior

Spot the latent version in review: a capture that was safe only because nobody mutated the referenced record yet, and say what you would capture instead.

for a principal

Decide whether deferred work in the codebase may capture mutable records at all, or must be handed finished values, and what that costs in allocation and ceremony.

## Two different changes wearing one word "The variable changed" hides two unrelated events, and almost every confusion about capture comes from mixing them: - **Reassignment.** The variable is made to hold something else. The old contents are untouched; the name now denotes a different thing. - **Mutation.** The variable holds a handle to a record, and a field inside that record is written. The variable's own contents are exactly what they were; the thing at the other end changed. Capture semantics is a rule about the first event only. It answers "does the closure see the variable's contents as they are now, or as they were at capture?" It has no opinion about the second, because the second never touches the variable. ## Why copying the variable is not copying the data When a language captures by value, it duplicates the storage the variable occupies. If the variable holds a number, the closure now owns an independent number. If it holds a handle, the closure now owns an independent **handle** — pointing at the same record everyone else is pointing at. Duplicating a handle duplicates the way to reach something; it does not duplicate the something. So after a by-value capture there are two handles and one record. A write through either handle is a write to the one record, and both sides read it back. Nothing was violated: the capture did exactly what it promised, which was to freeze the variable, not the world reachable from it. | Change made after capture | Closure that copied the value | Closure that kept the binding | |---|---|---| | A field of the referenced record is written | sees the new field | sees the new field | | A method on the record updates its own state | sees the update | sees the update | | The variable is assigned a different record | still sees the original record | follows to the new record | | The variable is assigned a plain new number | still sees the old number | sees the new number | The table has one row that differs, and that row is the entire observable content of capture semantics. How a language decides what a variable holds in the first place is a separate subject with its own rules; what matters here is that capture copies the contents, whatever they happen to be. ## How to actually get a frozen snapshot If the closure must report the state as it stood at capture time, capture something that cannot be written through: 1. **Pull out the values you need.** Read the specific fields into fresh locals immediately before creating the closure, and have the body use those. A number copied out of a record is genuinely independent of the record. 2. **Capture a finished value.** Build the thing the closure will report — a formatted line, a total, a small value whose parts cannot be rewritten — before the closure exists, and capture that. 3. **Pass it in instead of capturing it.** Hand the value to the function that builds the closure as an argument. Arguments are evaluated at that moment, so there is no live variable left for anything to change. All three share one shape: move the read **earlier**, to the moment you wanted the snapshot taken, instead of hoping the capture rule will do it for you. ## Where this bites in practice The expensive version of this bug is the one where the snapshot looked like it was working. A closure captures a record that nobody reassigns, so both capture models agree and the code behaves for months — until some path starts updating a field on that record in place, and every deferred reader begins reporting the newest state. Nothing about the capture changed; the record acquired a writer. The corresponding interview answer is short and precise: capture copies the variable, mutation goes around it, and the only way to be immune is to capture something that has no writable interior. A candidate who says "copying is a deep copy" has the wrong model and will write the bug; a candidate who says "a closure can never see later changes" has the same wrong model from the other side. ## The claim to state carefully Say it in the direction that is true: **a by-value capture protects against reassignment of the name, never against mutation of what it refers to.** Reversed — "copying the variable makes the data immutable inside the closure" — it is grammatical, confident, and wrong, and it is the single most common thing candidates say about this leaf.

  • In that example, which single step would the two capture models disagree about?
    Only the last one, where the variable is assigned a different record. A closure that copied the contents keeps reaching the first record; one that kept the variable follows to the second. Every earlier step is a write to a record both sides already reach.
  • How do you make the closure report the total as it was at capture time?
    Read the number out before creating the closure and capture that local instead of the record. A number copied out of a record has no writable interior, so no later write to the record can reach it, under either capture model.

saying these in an interview costs you the question

  • Capturing by value deep-copies everything the variable can reach.
  • If the closure saw the change, the language captures by reference.
  • A closure cannot observe mutation, only reassignment.
  • Copying the handle protects the record from later writes.
  • The two capture models disagree about a field write.