A loop builds one action handler per table row; every handler later acts on the last row. Why?
answer
- one variable, many functions
- capture, not a snapshot
- resolved at call time
- the loop reassigned that same binding
- leftover value is whatever ended the loop
basics
~20 sEvery handler captured the same loop variable rather than a copy of that pass's value. The loop reassigned that one binding on each pass, so by the time any handler ran it read whatever the loop had left behind.
solid answer
~40 sThe loop created **one** variable and reassigned it on every pass, so all the handlers closed over the *same binding* rather than over a snapshot of their own pass. A closure resolves a captured name when it is **called**, not when it is created, and these are all called after the loop has finished — at which point the single binding holds one value for everybody. That leftover value is not always the last row: a counted loop usually leaves the bound that ended it, which is one position past the last row. The repair is to give each pass a binding of its own — either a loop form that binds afresh per iteration, or an explicit copy into a local declared inside the loop body.
code
pseudocode · 9 lineshandlers = []
index = 0
while index < count(rows):
handlers.add(function() { show(index) }) // body names index; no copy taken
index = index + 1
// the loop ended because index reached count(rows)
for each h in handlers:
h() // every call shows count(rows)go deeper
Recall the one-line cause: the functions built in the loop share a single variable and read it when they run, so they all see what the loop left in it.
Be able to explain why the shared binding produces identical answers rather than off-by-one ones, and what value a counted loop actually leaves behind when it ends.
Show how you would recognise this from a bug report alone — all handlers agreeing, an out-of-range read, a green test — and what fixture size makes it reproducible.
Frame it as a review standard: a function value built inside a loop that names anything declared outside the body is worth a second look before it ships.
## The symptom A loop walks a list of rows. On each pass it draws the row and builds a small function — an action handler, a retry callback, a "show details" hook — whose body mentions the loop's variable. The handlers are stored, and something invokes them later. Every one of them acts on the same row. The signature is specific enough to recognise on sight: - **All the closures agree.** This is not an off-by-one on some rows; every single handler reports the same thing. - **The value they agree on is whatever the loop finished with**, which is frequently not a valid row at all. - **They are correct when called during the pass that created them**, and wrong the moment they are called after the loop. - **A fixture with one row hides the bug completely**, because there is only ever one value to report. ## One binding, reassigned What a closure holds on to is not a photograph of the surrounding scope. It holds a way of reaching the variables its body named, and it resolves them when it runs. In the buggy loop there is exactly one variable for the whole loop: 1. Before the first pass the loop creates one variable — the loop variable — and gives it a starting value. 2. On each pass the body builds a new closure whose body refers to that variable **by name**. Nothing copies the value out. 3. At the end of the pass the loop **reassigns** that same variable. The closure built on the previous pass is still pointing at it. 4. When the loop ends the variable holds one value, and every closure the loop produced resolves the name to that one variable — so every closure reads that one value. The loop really did produce N distinct closures: they are separate values, separately stored, separately invocable. They merely share a binding. The closures are fine; the binding is the bug. ## What the shared variable holds once the loop is over | loop shape | what the shared variable holds afterwards | what every deferred closure reports | |---|---|---| | a counted loop that stops when the index reaches the number of rows | the number of rows — the bound that ended it | an index one past the last row, often a failed lookup | | a loop that reuses one element variable while walking a collection | the last element visited | the last row, repeated | | a loop whose body updates a running total | the final total | the total, never the per-row partials | The first row is the one that surprises people. The reported value is not the last row; it is one position past it, so the first symptom is often an out-of-range read rather than a visibly wrong row. ## Why "it captured the value" is the wrong model here If capture had copied the value, each closure would carry its own number and the loop's later reassignment would be invisible to all of them. The behaviour you can actually observe rules that out: build a closure on the first pass, call it, let one more pass run, then call the same closure again. If its answer changed, the closure is reading a variable the loop is still writing. That experiment takes ten seconds and settles the argument in a review. It also explains why the bug is so quiet. Nothing fails at the moment the closures are built; the values are correct right up until the loop ends, and only the last write survives. ## Giving each pass a binding of its own Two shapes repair it, and both do the same underlying thing: - **Let the pass create the binding.** Some loop forms create a fresh variable for every iteration, so each closure closes over a different one. Whether a particular loop form does this varies between languages, and sometimes between loop forms within one language, so this repair depends on a guarantee you have to know you have. - **Copy the value into a local declared inside the loop body** and capture that local. Each pass executes the declaration, so each pass owns a variable, initialised while the value is still current and never reassigned afterwards. ## Reading code for it - Look for a function value created inside a loop whose body mentions a name declared **outside** the loop body. - Ask where that function is **called**. Called during the same pass, there is no bug; stored, queued or registered for later, there is. - Check the fixture before trusting a green test: with fewer than two rows, the correct and the broken version give identical output.
- Did the loop create one closure or one per row?One per row. Each pass built a separate function value with its own identity, so you can store them, compare them and invoke them independently. What they share is not the closure but the single variable their bodies name, which is why they all resolve it to the same value.
- If the handler reads the value at creation time and stores it in its own field, does the bug remain?No. Reading the variable while that pass is still current takes the value out of the shared binding, and the loop's later reassignment can no longer reach it. That is the explicit-copy repair, just written by hand inside the handler instead of as a local.
It is a stack of sticky notes that all read "see the whiteboard" instead of carrying a number each. Whatever is on the board when someone finally reads them is what every note says.
saying these in an interview costs you the question
- Says the closure copied the value at the moment it was created
- Blames the handlers firing out of order rather than the shared binding
- Assumes every loop form necessarily creates a new variable per pass
- Claims the leftover value is the last row without checking what ended the loop
- Suggests invoking the handlers sooner instead of fixing the binding