skip to content

Callbacks & Deferred Work

Passing a function for someone else to call later, and the control inversion that follows. Interviewers ask because deeply nested callbacks are the problem futures and coroutines were invented to fix.

on this pageshow

questions

4

A chat client hands a send routine a function to run on acknowledgement — what control has the caller given up?

level: juniorimportance: must knowfreq 70%

answer

  1. who is in charge afterwards
  2. a call became a registration
  3. whether, when, how many times
  4. the return path is no longer yours
  5. don't call us, we'll call you

basics

~20 s

The caller gives up control of invocation: the send routine now decides whether the acknowledgement handler runs at all, when it runs, how many times, with what arguments, and on which execution context. That reversal is inversion of control.

solid answer

~40 s

Passing a function to a send routine turns a call into a registration. The caller still decides *what* should happen on acknowledgement, but the routine owns *when* and *whether* it happens: it may invoke the handler once, never (the server never answers), twice (a retry acknowledged twice), or immediately, before the send call has even returned. The caller also loses the ordinary return path — the handler's return value goes back to whoever invoked the handler, not to the code that registered it, so results have to leave through state the handler captured or through a further call it makes. This reversal is **inversion of control**, nicknamed the Hollywood principle: don't call us, we'll call you. What the caller buys is decoupling — the send routine needs no knowledge of what acknowledgement means here.

code

pseudocode · 11 lines
pseudocode
function sendMessage(text, onAck)
    outbox.add(text)
    // sendMessage returns here; onAck has not run yet.
    // When the server answers, the transport calls: onAck(ackId)
end

sendMessage("hi", function(ackId)
    markDelivered(ackId)
end)

nextStatement()   // runs before markDelivered, not after

go deeper

for a junior

Be able to say, in one sentence, that you handed over behaviour and the receiver decides when it runs. Recall that the handler's return value does not come back to you.

for a middle

Explain the mechanics: registration versus invocation, who fills in the handler's arguments, why failures need their own agreed channel, and why the result has to leave through captured state.

for a senior

Show the judgment about when to accept the inversion at all: what the decoupling actually buys, and what it costs a reader who can no longer see the next step in the file they are reading.

for a principal

Frame it as an API-surface decision. Handing control to callers you cannot see means their handlers run on your schedule and inside your routine — decide what you are willing to guarantee them before you publish it.

## Two different things a call can mean When you write `markDelivered(id)` you are asking for work to happen now: the statement runs, it finishes, and the next statement runs after it. When you write `sendMessage(text, handler)` and `handler` is a function value rather than the result of calling one, you are not asking for the handler to run. You are **registering** it — handing a piece of behaviour to someone else so that they can run it at a moment they choose. That single change reverses who is in charge. The caller supplied the *what*; the send routine now owns the *when*. A function passed for someone else to invoke later is a **callback**, and the relationship it creates is **inversion of control**. ## What the caller no longer decides Once the function value is handed over, all of the following belong to the routine that received it: - **Whether** the handler runs at all — if the server never acknowledges, nothing ever calls it. - **When** it runs — later, or immediately, inside the registering call, before that call has returned. - **How many times** — once is the common contract, but a retried message acknowledged twice can invoke it twice, and a progress-style handler is invoked repeatedly by design. - **With what arguments** — the handler's parameters are filled in by the invoker, not by the registering code. - **On which execution context** — the same context that registered it, or a different one; nothing about passing a function value settles that. - **In what order** relative to other handlers the same routine holds. A junior candidate usually knows the word "callback". The question is whether they can say which of these the caller has actually surrendered. ## The return path disappears This is the consequence most people miss. A normal function's result flows back to its caller. A handler's caller is the send routine, so its `return` value goes there — and a routine invoking a handler for its side effect usually ignores whatever comes back. So the registering code cannot receive an answer the ordinary way. The result has to travel by one of: 1. **Captured state** — the handler writes into a variable or field the registering scope can read later, which is why callback-shaped code accumulates shared mutable state. 2. **A further handoff** — the handler itself calls the next piece of behaviour, passing the value along instead of returning it. 3. **A collaborator** — the handler pushes the value into an object both sides already know about. The same asymmetry applies to failure. In straight-line code an error can propagate up the call stack to the registering code. A handler invoked later is not on that stack at all, so failure has to be delivered deliberately — a second handler, an error-shaped argument, a status flag — or it is simply lost. ## Direct call versus handing over a function | | Direct call | Handing over a function | |---|---|---| | Who decides when the work runs | the caller, now | the receiving routine, later or immediately | | Where the result goes | back to the caller | to whoever invoked the handler | | How failure reaches the caller | up the call stack | only by an agreed channel | | What the callee must know | the work it performs | nothing about what the handler means | | How many times the work runs | exactly once per call | whatever the receiver's contract says | ## Why anyone accepts the trade Decoupling. The send routine is written once and knows nothing about chat bubbles, read receipts or analytics; every caller supplies its own meaning of "acknowledged". The alternative — the routine importing and calling each interested piece of behaviour by name — would force it to change every time a new caller appears. Handing behaviour in the other direction is what lets one generic routine serve callers it was never written for. The cost is that control flow stops being local. Reading the registering function no longer tells you what happens next, because what happens next is decided somewhere else, possibly at a time no line of this file mentions. That is the price paid for the decoupling, and it is why the nickname stuck: **the Hollywood principle** — "don't call us, we'll call you" — describes exactly the position the caller has put itself in. ## A note on names "Callback" describes the direction of the call, not the shape of the value. The thing passed may be a bare function value, an object with one agreed method, or a bundle of several named handlers. What makes it a callback is only this: the caller supplies it and someone else invokes it.

  • If the acknowledgement handler needs to produce a result for the code that registered it, where can that result go?
    Not through `return` — that value goes to whoever invoked the handler. It must travel through state the handler captured or was handed: a variable in the enclosing scope, a field, a collector object, or a further function the handler calls with the value. This is why callback-shaped code tends to accumulate shared mutable state.
  • Does registering a handler tell you which execution context it will run on?
    No. Passing a function value says nothing about where it will be invoked; that is part of the receiving routine's contract and has to be documented. Assuming it runs on the registering context is a common source of bugs, because the handler may touch state that only the original context is allowed to touch.
  • What is the Hollywood principle?
    "Don't call us, we'll call you" — the nickname for inversion of control. Instead of your code calling a component whenever it wants a result, you give the component your behaviour and it calls you on its own schedule. It names the relationship, not any particular mechanism for creating it.

You leave your phone number with a shop instead of standing at the counter. You still decide what you will do when they ring, but they decide when to ring, whether to ring at all, and how often.

saying these in an interview costs you the question

  • Thinking that passing the handler runs it, so the send call waits
  • Expecting the handler's return value to come back from the send call
  • Assuming a registered handler is always invoked exactly once
  • Believing a handler never runs before the registering call returns
  • Saying inversion of control means the caller keeps control of scheduling
open as a page

What must a callback-shaped API document about the handler it accepts, beyond that handler's parameters?

level: middleimportance: must knowfreq 56%

basics

~20 s

The invocation contract: how many times the handler may run, whether it can run before the registering call returns, on which execution context, how failures are delivered to it, and what the routine does if the handler itself throws.

open as a page

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

level: middleimportance: should knowfreq 50%

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.

open as a page

A chat client queues one zero-argument function per unsent message — what does holding work as function values cost the queue?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Opacity. A queued function value can only be run, never read, so the queue cannot deduplicate, prioritise, report on, persist or retry-with-different-arguments work it cannot inspect. Uniformity is what it buys in exchange.

open as a page