Futures, Promises, Callbacks and Completion
When you order at a busy counter, you get a receipt with a number.
Learning Objectives
Represent a future result with futures, promises and completion stages; use callbacks and continuations; chain and combine asynchronous operations.
Wait on futures, understand when that blocks, and handle error completion, exception propagation and unobserved exceptions.
Recognise callback hell, continuation ordering and executor association.
Apply cancellation, timeouts, completion races, shared results and promise resolution rules; use combinators (all, any, race, allSettled and equivalents); design APIs with futures and debug asynchronous stacks.
Plain-Language Explanation
When you order at a busy counter, you get a receipt with a number. The receipt is not your food, but it *stands for* your food: you can go and sit down, and when your number is called you exchange it. A future (or promise) is that receipt for a result that does not exist yet. You can attach instructions to it ("when it's ready, put sauce on it" — a continuation), wait for it, combine several, or be told that the kitchen burned it (an error).
Precise Technical Explanation
What represents a future result?
An object with three possible states — pending, fulfilled (with a value) and rejected/failed (with an error) — plus sometimes cancelled. Once settled it never changes.
The *read side*: std::future<T>, concurrent.futures.Future, java.util.concurrent.Future, Task<T> (.NET), Rust Future (a lazily polled computation — different: nothing runs until polled).
The *write side* that completes a future: std::promise<T>::set_value/set_exception, CompletableFuture.complete, TaskCompletionSource<T>, JavaScript's new Promise((resolve, reject) => …) / Promise.withResolvers(). In JavaScript "Promise" names the whole object; in C++ the two sides are separate.
Java's CompletionStage interface models a pipeline of dependent steps (thenApply, thenCompose, thenCombine, exceptionally, handle, whenComplete).
Functions invoked when an operation completes (Node's (err, result) => … convention, addEventListener, Future.add_done_callback). Simple, but composition and error handling get hard quickly.
A continuation is "the rest of the computation" to run after a result is available: .then, ContinueWith, thenApply, and implicitly everything after an await.
Chaining asynchronous operations .then(f) returns a *new* promise resolved with f's result — or, if f returns a promise, with *that* promise's eventual result (flattening; thenCompose in Java).
Combining multiple results Run independent operations concurrently and combine: Promise.all, CompletableFuture.allOf/thenCombine, Task.WhenAll, asyncio.gather.
Waiting on a future future.get(), .result(), .Wait(), .Result, .join(): block the calling thread until settled — returning the value or throwing the error. JavaScript has no blocking wait on the main thread; you await or chain instead.
Blocking on asynchronous work Blocking a thread on a future is fine in a plain worker thread, harmful on an event-loop thread (freezes everything) and can deadlock when the completion needs the thread you blocked (sync-over-async, Module 10; nested pool waits, Module 15).
A failed computation completes the future exceptionally; get() rethrows — Java wraps it in ExecutionException, C# .Result in AggregateException (await unwraps), Python re-raises the original.
Errors travel down a chain until handled: a rejected promise skips .then fulfilment handlers and reaches the next .catch; exceptionally/handle recover in Java.
Nested callbacks for sequential asynchronous steps ("pyramid of doom"), each with its own error check. Promises flatten it; async/await makes it look sequential.
Continuations attached to the same promise run in registration order (JavaScript) as microtasks after the current synchronous code finishes — never synchronously inside then. In Java and .NET a continuation may run synchronously on the completing thread or on a pool thread unless you specify an executor/scheduler.
Where does a continuation run? Java thenApplyAsync(f, executor), .NET TaskScheduler/SynchronizationContext/ConfigureAwait, Python loop.call_soon_threadsafe, C++ std::future has no continuations in the standard (the executors proposal is separate). Running heavy continuations on the wrong thread blocks timers, UIs or I/O threads.
Future.cancel() (Python/Java) succeeds only if the task has not started, or requests interruption; JavaScript promises have no built-in cancellation (use AbortController); .NET uses CancellationToken. Cancelling a future usually does not stop work already running elsewhere.
get(timeout), result(timeout=…), orTimeout/completeOnTimeout (Java 9+), Task.WaitAsync(timeout) (.NET 6+), Promise.race([p, timer]), AbortSignal.timeout(ms). A timeout stops *waiting*; whether the underlying work stops depends on cancellation support.
Two parties may try to complete the same promise (a result and a timeout). Promise APIs make completion idempotent: the first complete wins, later ones return false or are ignored — exactly what timeout and "first response wins" patterns need.
Shared future results std::shared_future, a Task awaited by many callers, a JavaScript promise with several .thens — one computation, many consumers; the basis of request deduplication (Module 18).
Promise resolution rules JavaScript: resolving with a thenable adopts its state; resolving twice is ignored; a then handler's thrown exception rejects the derived promise; handlers always run asynchronously (Promises/A+).
A failed future nobody waits on hides an error. Node emits unhandledRejection (and by default terminates the process since Node 15); .NET raises TaskScheduler.UnobservedTaskException on finalisation; Python logs "Future exception was never retrieved"; Java silently drops it.
Combinators: all, any, race, and equivalents all succeed, fail fast
Task.WhenAll (waits for all, then throws) gather / TaskGroup all settle, collect outcomes WhenAll + inspect tasks allOf + handle loop over WhenAny
anyOf (first completion, not first success) as_completed loop API design with futures
Return futures/tasks, not callbacks; never both block and return a future; document which executor continuations run on; propagate cancellation tokens; avoid "async void"/fire-and-forget; make results immutable.
Debugging asynchronous stacks Stack traces inside continuations show the scheduler, not the logical caller. Async stack traces (Chrome DevTools/Node --async-stack-traces default on,.NET's async-aware stacks, Python's asyncio task tracebacks, Java's limited support) stitch the logical chain together; correlation IDs in logs help across boundaries.
Why It Matters in Real Systems
Every modern API — HTTP clients, database drivers, cloud SDKs — returns futures or promises. Lost errors, blocked event loops and continuation-on-the-wrong-thread bugs are among the most common production problems in asynchronous code.
Key Terms
Read handle to an eventual result Write handle that completes a future (JS: the combined object)
Code scheduled to run when a result is available Fulfilled or rejected
Function combining several futures (all, any, race) Failure nobody awaited or handled Where continuations/tasks run
Syntax and API Overview
The specification's JavaScript example (Node.js 24):
delay returns a pending promise and schedules a host timer; nothing runs in parallel — after ~200 ms the timer callback resolves the promise, each .then handler runs as a microtask and returns a value that fulfils the next promise in the chain, and .catch would receive a rejection from any earlier step.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
delay(200)
.then(() => "finished")
.then(value => console.log(value))
.catch(error => console.error(error));Complete Code Example
Promise states, ordering, error propagation, combinators and unhandled rejections (Node.js 24):
CompletableFuture pipelines, Future.get and its exception wrapping (Temurin JDK 21):
const delay = (ms, value, fail = false) =>
new Promise((resolve, reject) => setTimeout(() => (fail ? reject(new Error(value)) : resolve(value)), ms));
process.on("unhandledRejection", (reason) => console.log("unhandledRejection caught:", reason.message));
async function main() {
console.log("1 sync start");
Promise.resolve().then(() => console.log("3 continuation runs after the current synchronous code"));
console.log("2 sync end");
await null; // let the microtask run
const chained = await delay(10, 2)
.then((x) => x * 10)
.then((x) => { throw new Error(`step failed at ${x}`); })
.then(() => "skipped") // fulfilment handlers are skipped after a rejection
.catch((e) => `recovered from: ${e.message}`);
console.log("chain:", chained);
console.log("all :", await Promise.all([delay(30, "a"), delay(10, "b")]));
console.log("allSettled:", (await Promise.allSettled([delay(10, "ok"), delay(5, "boom", true)])).map((r) => r.status + ":" + (r.value ?? r.reason.message)).join(", "));
console.log("any :", await Promise.any([delay(5, "fails", true), delay(20, "first success
…1 sync start
2 sync end
3 continuation runs after the current synchronous code
chain: recovered from: step failed at 20
all : [ 'a', 'b' ]
allSettled: fulfilled:ok, rejected:boom
any : first success
race : fast
all : rejected with all rejects fast
resolve twice: first
unhandledRejection caught: nobody handled thisimport java.util.concurrent.*;
public class Futures {
public static void main(String[] args) throws Exception {
try (var pool = Executors.newFixedThreadPool(2)) { // ExecutorService is AutoCloseable since Java 19
CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> 40, pool);
CompletableFuture<Integer> tax = CompletableFuture.supplyAsync(() -> 2, pool);
int total = price.thenCombine(tax, Integer::sum).thenApply(x -> x * 1).join();
System.out.println("thenCombine: " + total);
CompletableFuture<String> failing = CompletableFuture.supplyAsync(() -> { throw new IllegalStateException("db down"); }, pool);
System.out.println("exceptionally: " + failing.exceptionally(e -> "fallback because " + e.getCause().getMessage()).join());
Future<Integer> plain = pool.submit(() -> { if (true) throw new java.io.IOException("disk full"); return 1; });
try {
plain.get(); // blocks, then rethrows wrapped
} catch (ExecutionException e) {
System.out.println("get() threw ExecutionExcept
…thenCombine: 42
exceptionally: fallback because db down
get() threw ExecutionException caused by java.io.IOException: disk full
orTimeout: timed out (TimeoutException)
complete twice: true, false -> firstLine-by-Line Explanation
JS ordering: then callbacks never run synchronously; "3" prints after "2".
Chain: x * 10 gives 20; the throw rejects the derived promise; the next fulfilment handler is skipped; .catch recovers, so the whole chain fulfils.
Combinators: all keeps *input* order (["a","b"]) though "b" finished first; allSettled reports every outcome; any ignores rejections until one fulfils; race settles with the first to settle; all rejects as soon as any input rejects.
withResolvers: the second resolve is ignored — promises settle once.
Unhandled: the rejected promise with no handler triggers unhandledRejection (installed at the top; without it Node 24 would print the error and exit with a failure status).
Java: thenCombine joins two independent futures; exceptionally maps a failure (the original exception is wrapped in CompletionException, hence getCause()); Future.get wraps in ExecutionException; orTimeout completes exceptionally with TimeoutException; complete returns true only for the first call.
Output
Shown under each program; both are deterministic (timers are chosen far apart, and Java prints in program order after joins).
Interleaving or State Trace
Promise.all([delay(30,"a"), delay(10,"b")]):
pending (slot 1 = "b") fulfilled ["a","b"]
Correctness and Safety Analysis
Futures carry both value and error through one channel and settle exactly once, which removes whole classes of bugs (lost errors, double callbacks). The remaining risks are *unobserved* futures and continuations that run where they should not.
Common Mistakes and Misconceptions
Forgetting to return a promise from a .then handler (the chain continues before the inner work finishes).
Creating promises nobody awaits or handles.
Assuming Promise.all cancels the other operations on failure (it does not — they keep running).
Calling get()/.Result on an event-loop or UI thread.
Treating CompletableFuture.cancel as stopping the computation (it does not interrupt the running supplier).
Failure Modes and Debugging Steps
Missing result: check for a missing return in a chain or an unawaited promise.
Process exits with an error about an unhandled rejection: find the promise without a handler (Node prints the stack).
Hang: a future that is never completed — look for code paths that skip complete/resolve (especially error paths).
Performance and Scalability Implications
Each promise/future and continuation allocates; deep chains add scheduling hops. Combining independent operations with all/WhenAll turns sequential latency (sum) into concurrent latency (maximum).
Security Considerations
Unhandled rejections can crash a Node server (DoS); unbounded fan-out (Promise.all over user-supplied lists) can exhaust connections — cap concurrency.
Platform and Runtime Caveats
Promise.withResolvers needs Node 22+/modern browsers; Promise.any ES2021.
Java's ExecutorService try-with-resources requires Java 19+; on older versions call shutdown() and awaitTermination in finally..NET WhenAll waits for every task even after one fails, then throws the first exception (all are in Task.Exception).
Rust futures are lazy (nothing happens until polled by an executor) — unlike JavaScript promises, which start immediately.
Real-World Applications
Parallel API calls in backends, image loading in UIs, database query pipelines, scatter-gather search, request hedging and timeouts.
Best Practices
Return futures from asynchronous APIs and always observe them.
Use combinators for independent work; choose fail-fast or collect-all deliberately.
Specify the executor for continuations that do real work.
Put timeouts on every external wait; pair them with cancellation.
Log or propagate every failure exactly once.
Exercises
E1 · Beginner. What does Promise.all([p1, p2]) resolve to if p2 finishes first?
Answer: An array in input order: [value of p1, value of p2].
E2 · Intermediate. Explain why Promise.race([fetch(url), timeout(1000)]) does not cancel the fetch, and fix it.
Answer: race only settles the combined promise; the fetch keeps running. Pass an AbortSignal (fetch(url, { signal: AbortSignal.timeout(1000) })) so the request is actually aborted.
E3 · Advanced challenge. Implement firstSuccessful(tasks) in Java that completes with the first *successful* result or fails only when all fail.
Answer: Create a CompletableFuture<T> result; for each task whenComplete((v, e) -> { if (e == null) result.complete(v); else if (failures.incrementAndGet() == n) result.completeExceptionally(e); }) with an AtomicInteger failures — complete is idempotent, so only the first success wins.
Quiz
? A .then handler throws an error. What happens to the promise returned by that .then?
+ It is rejected with that error. :: Errors propagate down the chain.
- The error is ignored. :: Promises capture thrown errors.
- The original promise is rejected. :: The original stays fulfilled; the derived one rejects.
- Node.js crashes immediately. :: Only if the rejection is never handled.
? What does Java's Future.get() do when the task threw an exception?
+ Throws ExecutionException with the original exception as its cause. :: The cause carries the real error.
- Returns null. :: It rethrows.
- Throws the original exception unwrapped. :: Future.get wraps it.
- Blocks forever. :: The future is completed exceptionally.
? Which statements are true? (select all that apply)
+ Promise.race settles with the first input to settle, success or failure. :: race does not prefer fulfilment.
+ Promise.any fulfils with the first fulfilment and rejects only if all reject. :: With an AggregateError.
- Promise.all cancels the remaining operations when one rejects. :: They keep running.
+ A promise settles at most once. :: Later resolve/reject calls are ignored.Summary
Futures and promises represent results that do not exist yet, carry errors with values, settle once, and compose through chaining and combinators. Know where continuations run, never block an event loop on them, observe every failure, and remember that timeouts and combinators do not cancel work by themselves.
Further Reading
Promises/A+ specification; MDN: "Using promises", "Promise".
Java CompletableFuture and CompletionStage API documentation.
Microsoft Docs: "Task-based asynchronous pattern (TAP)".
Barbara Liskov & Liuba Shrira, "Promises: Linguistic Support for Efficient Asynchronous Procedure Calls" (PLDI 1988).