A handler running during a recalculation pass edits a cell and starts another pass — what can go wrong?
answer
- you do not call the loop
- the pass is not finished yet
- the edit is an unrecorded edge
- queue it instead of re-entering
- handlers promise no firing order
basics
~20 sThe nested pass sees a graph that is half-updated, so the handler reads values no settled state ever had. It can also re-trigger itself without bound, and its edit is a dependency the engine never recorded.
solid answer
~50 sEvent-driven style inverts control: you register a handler and the engine calls it — the Hollywood principle — so you choose neither when it runs nor in what order relative to other handlers. Re-entrancy is where that bites. The handler fires while the outer pass still has cells marked stale, edits a cell, and a nested pass begins over a graph that sits between consistent states. Three things follow: the handler can read a mix of old and new values; its edit can re-trigger the same handler, giving unbounded recursion or oscillation; and because the engine never recorded that edit as an edge, nothing will mark the affected cells stale next time. The fixes are to queue the edit until the pass settles, or better, to express the derived value as a cell definition so the edge becomes visible.
code
pseudocode · 11 lineson cell_changed(cell)
if cell.name = "average"
set "threshold" = cell.value * 1.1 # re-enters the running pass
# same handler, deferred instead of re-entering
on cell_changed(cell)
if pass_in_progress
enqueue(cell) # handled after this pass settles
return
if cell.name = "average"
set "threshold" = cell.value * 1.1go deeper
Recall that you register a handler and something else decides when to call it, so nothing about timing or ordering is yours to assume.
Explain why a handler that edits a cell mid-pass differs from one that edits it afterwards: the graph still holds stale cells, so a nested pass can read a mixture of values.
Separate the three problems — the recursion, the mid-pass read, and the dependency the engine never recorded — and say which remedy fixes which instead of reaching straight for a flag.
Set the boundary for the team: handlers carry effects out of the graph, definitions carry values inside it. That line is what keeps invalidation the engine's job rather than everyone's.
## Inversion of control, named In an event-driven loop you do not call the engine; the engine calls you. You supply a handler and register it, and from then on the framework owns the control flow — it decides when your code runs, in response to what, and, usually without promising anything, in what order relative to other handlers. That is the **Hollywood principle**, "don't call us, we'll call you", and **inversion of control** is the general name for it. What you gain is decoupling: the source of a change knows nothing about who reacts to it. What you give up is exactly the three things you would otherwise lean on — timing, ordering, and any guarantee that your code runs once per logical change. Everything a handler needs to know about "where we are" must be passed to it, or read defensively, because it cannot assume the world stood still while it was being called. ## Why re-entrancy is the hazard A recalculation pass is not atomic from a handler's point of view. During a pass some cells have been refreshed and others are still marked stale. If a handler fires in the middle of that and edits a cell, the engine starts a **nested** pass over a graph in that in-between condition. Three failure shapes come out of it: 1. **Reading a state that never settles.** The handler, and the nested pass it triggers, can observe a new value beside an old one — the same glitch a partial pass produces, but now caused by your own code. 2. **Unbounded re-entry.** If the handler's edit causes the event that invokes the handler, the recursion has no base case except whatever the engine imposes. It may exhaust the call stack, or oscillate between two values indefinitely, and neither failure names the handler as the culprit. 3. **An edge nobody recorded.** The handler makes one cell depend on another as code rather than as a definition. The engine cannot order that dependency, cannot de-duplicate it, and will not mark the target stale on some future change. The value then goes intermittently wrong and the sheet offers no clue why. | symptom | cause | fix | |---|---|---| | value correct on some edits only | the handler's edge is invisible | state it as a cell definition | | stack exhaustion on one edit | the handler re-triggers itself | queue the edit for the next pass | | a value briefly inconsistent | the handler ran mid-pass | run handlers after the pass settles | | a different result per run | handlers assume a firing order | remove the ordering assumption | ## Queue, guard, or re-model Three responses, in increasing order of how much they actually solve. - **A guard flag.** The handler checks whether a pass is in progress and returns if so. That stops the recursion and nothing else: the dependency is still invisible, and the skipped edit may simply never happen. - **Queue the edit.** The handler enqueues the change instead of applying it. The current pass finishes, the graph reaches a consistent state, and the queued edit then starts a fresh pass from that settled baseline. The cost is one extra pass and a window in which the derived value is visibly behind — so nothing may treat it as an invariant that holds at every instant. - **Make it a definition.** If the value is a function of other cells, say so. A definition puts the edge in the graph, which hands ordering, de-duplication and invalidation back to the engine — exactly the work the handler was doing badly by hand. The rule that falls out is worth stating plainly: **a handler is for effects that leave the graph** — notifying something, writing to a device, recording an audit line — because those cannot be expressed as a value. A handler that exists to keep one cell in step with another is a definition written in the wrong style. ## What an interviewer is checking The shallow answer is "add a flag so it does not loop". The strong answer separates the three problems: the recursion, which a guard or a queue fixes; the mid-pass read, which only deferring until the pass settles fixes; and the invisible dependency, which only re-modelling fixes. Candidates who have operated an event-driven system usually reach for the third unprompted, because the first two leave a value that is right most of the time — the worst of the three outcomes to debug.
- When should a derived value be a cell definition rather than a handler?Whenever the value is a function of other cells. A definition makes the dependency an edge the engine orders and invalidates for you; a handler hides it and leaves invalidation to you by hand, which is the part that rots. Keep handlers for effects that leave the graph — notifying, writing to a device, recording an audit entry — since those cannot be expressed as a value at all.
- What does the Hollywood principle actually cost you here?Control over timing and ordering. You register the handler and the engine calls it, so you cannot pick when it runs, rarely have a promise about its order relative to other handlers, and cannot assume it fires exactly once per logical change. Anything it needs about the current state must be given to it or read defensively, never inferred from the fact that it was called.
- How exactly does queuing the edit fix the mid-pass read?It moves the edit out of the current pass. The pass runs to completion, every stale cell is refreshed, and only then does the queued edit start a fresh pass from a consistent baseline, so nothing observes a mixture of old and new values. The price is an extra pass and a visible window in which the derived value lags, which callers must not treat as an always-true invariant.
saying these in an interview costs you the question
- Assumes handlers fire in the order they were registered.
- Thinks a handler always observes a fully settled graph.
- Says a recursion guard alone makes the handler correct.
- Treats a handler's edit as a dependency the engine can order.
- Believes re-entrancy only matters when two threads are involved.