skip to content

In a chat client, three acknowledgement handlers nested inside one another — what becomes hard to read and change?

level: middleimportance: should knowfreq 50%

answer

  1. sequence became indentation
  2. nothing runs after the outermost registration
  3. each level carries its own failure path
  4. return exits the handler, not the sequence
  5. flattening costs you the captured values

basics

~20 s

Sequence stops being expressed by order of lines and becomes indentation. Each level needs its own failure path, no statement means "after all three steps", early return no longer abandons the sequence, and the step count is fixed at authoring time.

solid answer

~40 s

Each nested handler is a separate function, and that is what breaks. **Sequence is now indentation**: step two lives inside step one's handler, so reading the steps means reading inwards rather than downwards. **Failure is per level** — a handler registered at depth three is not covered by anything wrapped around the outer registration, so each level passes its own failure path and they drift apart. **There is no "afterwards"**: no line in the enclosing function runs after the third step, because the enclosing function returned long before. **Early exit is gone** — a `return` inside a handler returns to whoever invoked that handler, not out of the sequence. And you cannot loop: nesting is written at authoring time, so a variable number of steps needs a driver rather than syntax.

code

pseudocode · 9 lines
pseudocode
sendMessage("hi", function(ack1)
    sendMessage("how are you", function(ack2)
        sendMessage("bye", function(ack3)
            markAllDelivered(ack1, ack2, ack3)
        end, onFailure)
    end, onFailure)
end, onFailure)

logDone()   // runs first, not last

go deeper

for a junior

Recognise the shape and be able to say why it is hard to read: the later a step runs, the deeper it sits, and code written below the outermost registration runs first, not last.

for a middle

Explain each lost property — sequence, failure coverage, early exit, looping — and show the flattening, including the values you now have to thread forward by hand.

for a senior

Judge when to flatten and how far: which sequences deserve named steps, which deserve a driver over a list, and what a shared failure handler must do that the per-level ones were doing inconsistently.

for a principal

Recognise the pattern as a signal about the API surface your teams are handed. Whether sequencing is expressible linearly is a house-wide decision, not a per-file cleanup.

## Why three levels feel so much worse than one One handler is easy to read: do this, and when the answer comes, do that. The trouble starts when the second step also needs an answer, so its registration goes *inside* the first handler, and the third goes inside the second. Nothing about any single level is complicated. What degrades is every property that made straight-line code readable, and it degrades all at once. ## What is actually lost - **Order of operations.** In straight-line code the next step is the next line. Here the next step is one level further in, so the reader follows indentation, and the deepest, latest step sits in the least visible place on the screen. - **A place that means "afterwards".** Code written after the outermost registration does not run after the sequence — it runs almost immediately, before any step has finished. There is no statement in the enclosing function that means "once all three are done"; that meaning can only live at the innermost level. - **Uniform failure handling.** A handler is invoked from somewhere else entirely, so wrapping the outer registration in an error guard protects the registration, not the steps. Each level supplies its own failure path, and in practice they diverge: one logs, one retries, one silently does nothing. - **Early exit.** `return` inside a handler exits that handler. It does not abandon the remaining steps, because those steps are registered by code that has already run or has not run yet. Abandoning the sequence has to be modelled explicitly, usually with a flag every level checks. - **Ordinary control flow over the steps.** A loop cannot nest handlers, because nesting is fixed when the code is written while a loop is a run-time repetition. Ten steps means ten levels, or a driver. - **Locality of variables.** The innermost handler can see every enclosing level's variables, which is convenient until you need to know which step a variable belongs to, or until two levels want the same name. ## What flattening buys and what it costs The first move is to stop nesting anonymous handlers and give each step a name at the top level, so the pyramid becomes three sibling functions that each register the next. Reading is restored — the steps sit side by side, and each is small enough to test on its own. One shared failure handler can then be passed to every registration instead of a fresh one per level. The cost is real and worth stating, because a candidate who claims flattening is free has not done it. Nesting gave the inner levels **free access to every outer level's values**. Once the steps are siblings, nothing is enclosing anything, so every value a later step needs must be threaded through explicitly as a parameter. Deep sequences therefore grow parameter lists, and the usual answer is to thread one small record instead of many separate values. | | Three nested handlers | Three named steps | |---|---|---| | Reading order | inwards, by indentation | downwards, by name | | Earlier values | visible for free | passed explicitly | | Failure path | one per level, drifting | one handler shared | | Variable step count | impossible | a driver over a list | | Testing a middle step | requires the outer two | in isolation | ## Driving a variable number of steps When the count is not known at authoring time, the shape that works is a small driver: 1. Hold the steps as a list of function values, each taking the state so far and a function to call to continue. 2. Run the first one, handing it a continue-function that advances the index. 3. When the index runs off the end, call the finish handler; on any failure, call the shared failure handler instead. This is **continuation-passing style**: each step receives, as an argument, the rest of the work. It flattens arbitrarily deep sequences into one loop-free driver whose depth does not grow with the number of steps, and it makes "afterwards" a real place again — the finish handler. ## Keeping the diagnosis honest The pyramid is not a formatting problem, and no amount of reformatting fixes it: the difficulty is that sequence, failure and exit have each been re-expressed in a structure that was designed for none of them. That is also why later abstractions for sequencing deferred work were introduced — they restore a linear reading of the steps. Their mechanics are another subject; what belongs here is being able to say precisely which properties the nesting took away, because that list is exactly what any replacement has to give back.

  • Why doesn't wrapping the outermost registration in an error guard cover the nested steps?
    The guard protects the code that runs inside it, and that is only the registration. Each nested handler is invoked later, from the send routine, with the guard long gone from the call stack. Coverage has to be arranged per registration — which is why one shared failure handler passed to every level is the practical fix.
  • How would you sequence a number of steps that is only known at run time?
    Hold the steps as a list of function values and write a driver that runs index `i`, handing each step a continue-function that invokes index `i + 1`. Depth stays flat however long the list is, and the end of the list is the single place that means "afterwards". That shape is continuation-passing style.
  • What did the nesting actually give you that the flattened version has to pay for?
    Enclosing scope. Each inner handler could see every value from the levels around it without being handed anything. Once the steps are siblings, those values must be passed forward explicitly, so deep sequences grow parameter lists and usually end up threading one small record of accumulated state instead.

saying these in an interview costs you the question

  • Calling nesting a formatting problem that indentation tooling solves
  • Thinking a return inside a handler abandons the whole sequence
  • Believing one outer error guard covers every nested step
  • Claiming a loop can nest a run-time number of handlers
  • Assuming flattening keeps the inner steps' captured values for free