skip to content

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

level: middleimportance: must knowfreq 56%

answer

  1. the signature is not the contract
  2. count, timing, context, failure
  3. may it run before registration returns
  4. what if the handler itself throws
  5. one ordering, always, or callers cannot win

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.

solid answer

~50 s

A handler's signature says what it receives; it says nothing about how it will be treated. The API has to publish the rest: **how many times** it may be invoked (exactly once, at most once, many times), **when** — in particular whether it may be invoked synchronously, before the registering call has returned — **where**, meaning which execution context it runs on, **how failure arrives**, since an error cannot propagate up a stack the handler is no longer on, and **what happens if the handler throws**: swallowed, logged, or propagated into the routine that invoked it. Two more matter in practice: whether the routine keeps its reference to the handler after the final invocation, and whether the handler is allowed to call back into the same API. Leave any of these unstated and every caller invents its own defensive answer.

code

pseudocode · 10 lines
pseudocode
function sendMessage(text, onAck)
    if alreadyAcknowledged(text)
        onAck(cachedId)        // runs BEFORE sendMessage returns
        return
    end
    outbox.add(text, onAck)    // runs later, after sendMessage returns
end

sendMessage("hi", function(id) pending = false end)
pending = true                 // wrong in the cached branch

go deeper

for a junior

Know that the handler's parameter list is not the whole agreement, and that you must read how many times it can run and how errors reach you before using such an API.

for a middle

Enumerate the clauses and explain the mechanics behind each: why failure needs its own channel, why the count matters for side effects, why a varying timing is unprogrammable.

for a senior

Diagnose from symptoms — an effect that happens twice, state that is stale only sometimes, a lost failure — and trace each back to the specific unstated clause that allowed it.

for a principal

Treat the contract as the published surface: every clause you leave unstated becomes a guess in every consumer, and tightening it later breaks the ones that guessed differently.

## The signature is the smaller half of the contract An API that accepts a function writes down the handler's parameters and result type, and readers assume that is the contract. It is not. The parameters describe what the handler will be *given*; they say nothing about the treatment it will *receive*. Everything that makes callback-shaped code hard is in the unwritten half, because the caller has surrendered invocation and now has to program against someone else's undocumented habits. ## The clauses that have to be written down - **Arity of invocation.** Exactly once, at most once, at least once, or many times? A completion handler and a progress handler have identical signatures and opposite contracts. If a caller assumes "once" and the routine retries, side effects double. - **Timing relative to registration.** May the handler be invoked *before* the registering call returns? A routine that answers from a cache synchronously but from the network later has two orderings, and callers that set up state after the registering call will sometimes find that state already stale. - **Execution context.** Which context the handler body runs on. Nothing about passing a function value decides this, and languages and runtimes differ in what they even provide here. - **The failure channel.** The handler is not on the registering code's call stack, so an error in the operation cannot propagate there. The API must say how failure arrives: a separate failure handler, a failure-shaped first argument, a status value, or an exception thrown from the registering call for argument errors only. - **A throwing handler.** If the handler itself raises, the invoking routine decides the outcome — swallow it, log it, abandon the remaining handlers, or let it escape into whatever invoked the routine. All of these exist; none is inferable. - **Retention.** Whether the routine drops its reference after the last invocation or keeps it until the caller withdraws it. - **Reentrancy.** Whether the handler may call back into the same API while it is being invoked. ## The inconsistent-dispatch hazard Of these, the second is the one interviewers press on, because it produces bugs that reproduce only sometimes. Consider a send routine that invokes the acknowledgement handler immediately when the message was already acknowledged, and later otherwise. Now this caller is wrong half the time: 1. It calls the send routine, passing a handler that clears a `pending` flag. 2. It sets `pending = true` on the line *after* the registering call. 3. In the cached case the handler already ran and cleared the flag, and step 2 sets it back to true — permanently. The defect is not in either branch. It is in the API offering two orderings for one handler, which forces every caller to be correct under both. An API that picks one ordering and documents it — always deferred, or always immediate for the cached case — is programmable; one that varies is not. ## How the clauses compare | Clause | What a caller does if it is documented | What happens if it is not | |---|---|---| | Invocation count | makes the handler safe for that count | guards every handler for repeats, or ships double effects | | Timing | orders its own setup accordingly | writes setup that is correct under only one ordering | | Context | touches only state that context may touch | corrupts state reached from the wrong context | | Failure channel | handles failure in the agreed place | silently loses failures | | Throwing handler | knows whether to catch inside the handler | lets an exception take down an unrelated routine | ## Designing the contract rather than discovering it Three habits make the contract cheap to honour. **Pick one timing and never vary it** — even when an answer is already available, deliver it the same way as any other, so callers have one ordering to reason about. **Give failure a channel as explicit as success** — if the only way to learn something went wrong is that the success handler never ran, callers cannot distinguish failure from a slow answer. **Say what you do with a throwing handler**, and prefer containing it: a routine holding several handlers that abandons the rest because one raised will surprise every other caller. The underlying point is the same one the whole subject turns on. Handing someone a function means handing them the decision to call it, and a decision that is not written down is still being made — just by whoever wrote the routine, in a way nobody else can see.

  • Why can't the operation's failure simply propagate to the code that registered the handler?
    Because that code is no longer on the call stack when the operation finishes. Propagation carries an error up through active calls, and the registering call has already returned. Failure therefore needs a channel the API defines — a second handler, a failure-shaped argument, or a status value the success handler inspects.
  • A routine holds several registered handlers and one of them throws. What are the reasonable policies?
    Contain it — log it and continue with the remaining handlers — or abandon the round and propagate. Containment keeps one bad caller from silencing the others and is the usual default; propagation is defensible only when the routine's own state is now inconsistent. What is not defensible is leaving the choice undocumented.
  • Is it ever fine for an API to invoke a handler synchronously?
    Yes, as long as it always does, and says so. The hazard is variation, not immediacy: callers can write correct setup for either ordering, but not for an ordering that depends on cache state they cannot see. Consistency is the property worth guaranteeing.

saying these in an interview costs you the question

  • Assuming any handler an API accepts is invoked exactly once
  • Believing a callback is always invoked after the registering call returns
  • Expecting failures to surface as an exception from the registering call
  • Thinking an exception inside the handler unwinds to the registering code
  • Assuming the handler always runs on the caller's own execution context