Low-level designCore8 min
Futures and async
A future is a handle to a result that isn't ready yet. Start several slow calls, hold their futures, and wait for all of them together: the total wait becomes the slowest call, not the sum. Then decide what a failure in one of them means for the rest.
Three quotes for one trip
TripPlanner asks a flight, a hotel and a car service for quotes and adds them up.
1 · The situation
Three quotes for one trip
A travel site builds a trip from three quotes: a flight, a hotel and a rental car, each from a different remote service. Each call is slow: here 200, 300 and 100 milliseconds, simulated by sleeping. TripPlanner.plan() asks for the three and adds them up. The first version asks one after another.
class TripPlanner {
private final QuoteService flight;
private final QuoteService hotel;
private final QuoteService car;
TripPlanner(QuoteService flight, QuoteService hotel, QuoteService car) {
this.flight = flight;
this.hotel = hotel;
this.car = car;
}
String plan() {
int f = flight.quote();
int h = hotel.quote();
int c = car.quote();
return "Flight $" + f + ", hotel $" + h + ", car $" + c + ": total $" + (f + h + c);
}
}Each program times itself and checks the time against the sum of the three waits, 600 milliseconds; exact times vary from run to run, so it prints only the comparison.
Flight $420, hotel $310, car $95: total $825 Took at least the sum of the waits: yes
2 · The problem
Waiting for each call in turn
The three calls don't depend on each other: the hotel quote doesn't need the flight's price. Yet the planner waits for the flight before it even asks for the hotel. The trip takes as long as all three calls added together, and every service the site adds makes it slower still.
The waits are 200, 300 and 100 ms. Done in turn, they add up; done together, the trip waits only for the hotel.
3 · The fix
Start all three, then wait for all three
Start every call first, keep a handle to each result, and only then wait. The handle is a future, or a promise in JavaScript: an object that will hold the result, or the error, once the call finishes.
Java starts each call with CompletableFuture.supplyAsync() and waits with join(). C++ uses std::async, which runs each call on its own thread and returns a std::future. JavaScript passes the three promises to Promise.all(), and Python passes the three coroutines to asyncio.gather().
class TripPlanner {
private final QuoteService flight;
private final QuoteService hotel;
private final QuoteService car;
TripPlanner(QuoteService flight, QuoteService hotel, QuoteService car) {
this.flight = flight;
this.hotel = hotel;
this.car = car;
}
String plan() {
CompletableFuture<Integer> f = CompletableFuture.supplyAsync(flight::quote);
CompletableFuture<Integer> h = CompletableFuture.supplyAsync(hotel::quote);
CompletableFuture<Integer> c = CompletableFuture.supplyAsync(car::quote);
int fp = f.join();
int hp = h.join();
int cp = c.join();
return "Flight $" + fp + ", hotel $" + hp + ", car $" + cp + ": total $" + (fp + hp + cp);
}
}Flight $420, hotel $310, car $95: total $825 Took less than the sum of the waits: yes
The same trip, in about the time of the slowest call.
Flight $420, hotel $310, car unavailable: total $730 Took less than the sum of the waits: yes
One optional call failed; the rest of the plan stands.
The planner holds three services and asks all three at once; what it does with each answer is its own decision.
4 · The idea
Futures, promises, and async functions
A future separates starting work from using its result. The caller gets the future at once, carries on, and later waits on it; the work runs meanwhile, on another thread or while the program waits on the network. Async functions (async and await in JavaScript and Python) are a way of writing code that works with futures as if it ran step by step: await pauses the function until the result is ready, and lets other work run in the meantime.
All three run at once. The planner waits once, and the wait ends when the slowest answer arrives.
Language | Start a call | Wait for several | Runs on |
|---|---|---|---|
Java |
|
| A thread pool, the common pool by default |
C++ |
|
| A thread per call |
JavaScript | Call an async function: it returns a promise |
| One thread; waiting happens in the event loop |
Python | Call a coroutine; |
| One thread; waiting happens in the event loop |
Calling a coroutine or an async function in a sequence of awaits still runs the calls one after another, as the first version did. Concurrency comes from starting them all before waiting on any.
5 · Failures
When one of several fails
Waiting for several calls raises a question the sequential version never had: if one fails, what happens to the others? The common combinators answer differently:
Combinator | When one fails | The others |
|---|---|---|
| Rejects at once with the first error | Keep running; nothing cancels them |
| Never rejects; reports each result or error | All finish |
| Raises the first error, unless | Keep running |
| Raises an | Canceled as soon as one fails |
Work that keeps running after nobody wants its result wastes resources, and an error in it can go unseen. Structured concurrency ties child tasks to the block that started them: the block doesn't end until every child has finished or been canceled, and a failure in one cancels its siblings. Python's TaskGroup works this way; in other languages, you cancel the remaining work yourself when the result is no longer needed.
6 · When breaking it is fine
Not everything should run at once
Concurrency has costs of its own:
- Dependent calls must wait. If the hotel search needs the flight's arrival time, it can't start until the flight answers; forcing them together gives wrong results, not fast ones.
- Load on the services. Starting a hundred calls at once can overwhelm a service that handled them one by one. Limit how many run together.
- Harder failures. Sequential code fails at one clear point; concurrent code can have several failures at once, and they must all be reported or handled.
Keep calls sequential when each needs the previous result, when there are only one or two fast calls, or when the order of side effects matters, such as charging a card before booking the seat.
7 · Getting it right
Always wait, and always decide
- Never drop a future. A future nobody waits on hides its errors; in C++, the future returned by
std::asynceven blocks in its destructor, which can quietly make "concurrent" code sequential. - Decide each call's failure policy: required, so it fails the whole; optional, with a fallback; or retried.
- Put timeouts on remote calls, so one stuck service can't hold the whole plan forever.
- Don't block an event loop. In JavaScript and Python, a long computation inside an async function stops every other task; move it to a worker thread or process.
In Java, join() blocks the calling thread, which is fine on an ordinary thread. In a highly concurrent server, chain the next step with thenCombine() or thenApply() instead, or use virtual threads, where blocking is cheap.
8 · Look-alikes
Ways to wait for many things
Several tools wait for work that happens elsewhere.
Idea | What it gives you |
|---|---|
Future or promise | One result that will arrive later |
Callback | A function called with the result; the older style futures replaced |
Thread pool | Where Java's and many other futures run their work |
Producer-consumer | A stream of results through a queue, rather than one result per call |
Reactive streams | Many results over time, with backpressure |
9 · In practice
What the documentation shows
Python's asyncio documentation demonstrates this page's arithmetic: two coroutines awaited in turn take the sum of their waits, and started as tasks first they take the longer one. It also recommends TaskGroup over gather() because a failure cancels the remaining tasks. MDN notes that Promise.all() rejects with the first error without canceling the other operations.
Any page or API that gathers data from several services, such as a dashboard, a search across sources, or a checkout that prices shipping, tax and discounts, has this shape.
Check yourself
4 questions
Takeaways
Remember this
- A future or promise is a handle to a result that arrives later; starting work and using its result become separate steps.
- Start independent calls together and wait together: the total wait is the slowest call, not the sum.
- Awaiting calls one after another is still sequential; concurrency comes from starting them before waiting.
- Decide each call's failure policy; combinators differ on whether one failure stops or cancels the rest.
- Never drop a future, put timeouts on remote calls, and don't block an event loop.