skip to content

Asynchronous work can be expressed by passing a callback that is invoked on completion, or by returning a future object that the caller composes. What does the future style buy you over raw callbacks, and what does it cost?

level: juniorimportance: should knowfreq 55%

answer

  1. Callback = you pass the continuation in
  2. Future = pending result becomes a value
  3. Pyramid, ad hoc errors, called-twice
  4. One error channel, combinators, awaitable
  5. Cost: allocation, stack traces, still callbacks underneath

basics

~20 s

A future is a first-class value: you can store, return, and combine it, and it has one defined outcome — value or error. Callbacks invert control, nest as they compose, and each one invents its own error and completion convention. Futures cost allocation and clarity of stack traces.

solid answer

~50 s

With a callback you hand your continuation to the callee, so the callee decides when, where, and how many times to call you back. That has three consequences: composition nests (each dependent step is another level of indentation), errors travel by convention (an error argument that is easy to ignore), and completion is unverified — a buggy source may call you twice or never. A future turns the pending result into a **value**. Because it is a value, you can return it from a function, keep it in a map, hand it to a combinator that waits for ten of them, or await it in code that reads top to bottom. The single-assignment rule normalizes completion (exactly once), and errors become a first-class channel that propagates down the chain instead of being re-handled at every level. The cost: an object per step, longer machinery in stack traces, and the fact that callbacks still exist underneath — the future just standardizes them.

code

text · 14 lines
text
# callback style: nests, error handling repeated
loadUser(id, (err, user) -> {
  if (err) return done(err)
  loadOrders(user, (err, orders) -> {
    if (err) return done(err)
    price(orders, (err, total) -> done(err, total))
  })
})

# future style: flat, one error path
loadUser(id)
  .chain(user   -> loadOrders(user))
  .chain(orders -> price(orders))
  .onError(e    -> report(e))

go deeper

for a junior

Name the concrete wins: no nesting pyramid, errors on one channel, and the result is a value you can pass around.

for a middle

Add once-only completion, combinators, and the fact that await-style syntax needs the reified handle to exist.

for a senior

Discuss the costs honestly — allocation per step, lost stack causality, and that the never-called-back bug becomes the never-completed bug.

for a principal

Position it as an API-surface decision for a platform: one shared async currency across libraries buys interoperability, and the standardization matters more than any single ergonomic win.

## Two ways to say *later* Both styles solve the same problem — the result is not ready now — but they differ in **who holds the continuation**. - **Callback style:** you pass a function inward. The producer owns it and invokes it whenever the result exists. Control is inverted: the callee schedules you. - **Future style:** the producer returns a handle immediately. You keep control and decide what to attach, when, and whether to combine it with other pending results. ## What goes wrong with raw callbacks **1. Composition nests.** Every step that depends on the previous one lives inside the previous callback. Three sequential calls become three levels of indentation; add a conditional branch and the shape becomes hard to read and harder to modify. This is the *callback pyramid*. **2. Errors are ad hoc.** Some APIs pass an error as the first argument; some take two callbacks; some throw synchronously for argument validation and call back for I/O failure. Every level must remember to check and forward. A missed check silently swallows a failure, and the symptom appears far away as a request that never finishes. **3. Completion is not guaranteed.** Nothing in the callback contract says *exactly once*. Real defects include calling back twice on a retry path and never calling back on an early return. Consumers defend with their own *have I already been called* flags. **4. Callbacks are not values.** You cannot say *give me the result of these five operations when all are done* without writing a counter and a shared mutable accumulator by hand, with its own memory-visibility problem across threads. **5. Context is invisible.** Because the callee decides which thread runs your callback, thread-affinity assumptions and thread-scoped context are silently broken with no place in the code that shows the hop. ## What futures change **Reification.** The pending result becomes an ordinary value with a type. Anything you can do with a value you can now do with an in-flight computation: return it, store it, pass it, put it in a list. **Standard completion.** One outcome, exactly once, value or error, permanent. Consumers stop writing defensive once-only flags. **A single error channel.** A failure short-circuits the rest of the chain and arrives at whichever handler is attached downstream, so intermediate steps do not need error plumbing at all. **Combinators.** *Wait for all of these*, *take the first to finish*, *transform the value*, *chain another async step* become library functions rather than hand-rolled counters. **A foundation for sequential syntax.** Await-style language features are built on top of a future-shaped abstraction: they let you write code that looks sequential while suspending on the handles. Callbacks alone cannot be awaited without the intermediate object. ## What futures cost - **Allocation and scheduling overhead.** Each step allocates a handle and a continuation record. For very hot, very fine-grained operations, a callback is cheaper — this is why the lowest layers of some runtimes stay callback-based and expose a future facade upward. - **Diagnostics.** The physical stack no longer tells the causal story: the frames at failure time belong to the completion machinery, not to the code that started the operation. Good libraries reconstruct an async causality chain, but it is extra machinery you must know how to read. - **They are still callbacks.** A future does not eliminate the inverted control, it standardizes it. Whoever completes the future still runs the continuations, so *what thread am I on now* remains a real question in both styles. - **Leaks move around.** With callbacks the classic bug is *never called back*; with futures it is *never completed*, which looks the same to the consumer. ## How to answer in an interview Say that both are the same mechanism, but a future reifies the pending result as a value, which is what makes composition, uniform error propagation, and once-only completion possible; and that the price is allocation plus harder stack traces. If you also note that await-style syntax is only possible because the handle exists, you have shown why the industry moved that direction.

  • If futures are implemented with callbacks underneath, what did we actually gain?
    We gained a standard, inspectable object between producer and consumer. That object enforces once-only completion, carries a uniform error channel, and can be stored and combined, which is what makes generic combinators and await-style syntax possible. The inversion of control is still there, but it now happens at one well-defined place instead of in every API's own convention.
  • When would you still prefer a plain callback?
    For very hot, fine-grained operations where the per-step allocation matters, and for sources that deliver many values over time rather than one — a future is single-assignment, so repeated notification does not fit it. Low-level runtime layers also stay callback-based deliberately and expose a future-shaped API to their users.

A callback is leaving your phone number with a shop and hoping they ring. A future is a numbered ticket: you can hold it, hand it to a friend, or queue five tickets and wait for all of them.

saying these in an interview costs you the question

  • Claiming futures remove the inversion of control rather than standardizing it
  • Saying futures make the code concurrent, when they only describe when a result arrives
  • Treating the callback pyramid as merely a style complaint and missing the error-plumbing and called-twice problems
  • Assuming a future guarantees a non-blocking implementation underneath
  • Believing futures cost nothing — ignoring per-step allocation and degraded stack traces

context