AlgoMaster Logo

Futures & Promises (std::future, std::promise)

Medium Priority16 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

When one thread needs to hand a single value to another, building it from mutexes and condition variables is more work than it should be. std::future and std::promise are the higher-level pair the C++11 standard library adds for this case: one thread produces a value (or an exception), and another thread waits for it. This chapter covers how the two ends connect, how exceptions cross the thread boundary, and when to use std::shared_future to fan a single result out to several consumers.

The One-Shot Value-Return Problem

Threads communicate by sharing memory, but raw shared memory is awkward when the only goal is "thread A computes a number, thread B reads it once." That setup needs a std::mutex to guard the variable, a bool to signal completion, a std::condition_variable to wake the reader, and some discipline about ordering. A small mistake produces a race or a hang.

A concrete example: an e-commerce checkout fires a request to the payment gateway on a background thread. The main thread is rendering the confirmation page and needs the gateway's authorization code before it can finish. The main thread shouldn't block on a mutex, then keep checking a flag, then sleep on a condition variable. It wants to say "give me the result when it's ready" and let the standard library deal with synchronization.

std::future and std::promise are that abstraction. The promise sits in the producing thread and writes a value once. The future sits in the consuming thread and reads that value once. The two are connected through a hidden shared state that the standard library manages.

The shared state is the piece that stays hidden. It holds the result (or an exception), a flag that says whether it's been set, and the synchronization primitives the library uses internally to make get() block correctly. Code never touches the shared state directly. One side holds a promise, the other holds a future, and the connection happens through promise.get_future().

Compared with condition variables, futures and promises are a higher-level alternative for the specific case of returning a single result. Condition variables are still useful for ongoing communication, multiple events, or a long-lived producer/consumer queue. For one-shot answers, the future/promise pair is much less code.

A future/promise pair is one-shot. The promise can fulfill the shared state exactly once, and a plain std::future can read it exactly once. To reuse either side, either rebuild the pair or use std::shared_future.

A Minimal Example

The smallest end-to-end example. The producer thread computes a shipping cost and stores it. The main thread waits for the result.

Three details are worth pointing out. First, costPromise.get_future() is what links the two ends. Call it once on the promise and hand the returned future to whoever will be waiting. Second, the promise is moved into the worker thread because std::promise is move-only (it owns the shared state and shouldn't be copied). Third, costFuture.get() blocks the main thread until the worker calls set_value, then returns the stored value. The blocking happens inside the standard library; the wait loop isn't written by hand.

Without the std::move, the build won't compile because std::promise has its copy constructor deleted. The compiler error from g++ looks like use of deleted function 'std::promise<double>::promise(const std::promise<double>&)'.

std::future<T>: The Consumer Side

std::future<T> is the read end. It has a small but precise interface, and getting the corners right matters because misusing it is a common cause of bugs.

get(): Read the Value Once

get() blocks until the shared state becomes ready, then returns the stored value (or throws the stored exception). After get() returns, the future is no longer valid; a second call to get() is not allowed.

A second call to get() produces a std::future_error with the future_errc::no_state error condition, because the first call invalidated the future. The fix when multiple reads are needed is std::shared_future, covered later.

wait(): Block Without Consuming

wait() blocks the calling thread until the shared state is ready but does not extract the value. Use it to know the result is available without reading it yet, or when the value type is void and there's nothing to extract.

A std::future<void> is the right pattern when the producer only needs to signal completion (the label was printed, the cache was warmed, the transaction was committed) and there's no value to hand back. set_value() with no argument signals the void future.

wait_for() and wait_until(): Timed Waits

Sometimes blocking forever isn't acceptable. wait_for(duration) waits up to the given duration; wait_until(time_point) waits until a specific clock time. Both return a std::future_status enum indicating why they returned.

The future_status enum has three values:

ValueMeaning
std::future_status::readyThe shared state is set; get() will return immediately
std::future_status::timeoutThe timeout elapsed before the state became ready
std::future_status::deferredThe result is from a deferred std::async, not yet started

The third value, deferred, only appears with std::async(std::launch::deferred, ...). For futures created from a std::promise, only ready and timeout show up.

A timed wait is cheap when the value is already there. When it has to sleep, the implementation typically blocks on a condition variable internally, the same way one would write it by hand. The timeout precision is whatever the platform's clock provides.

valid(): Is There a Shared State?

A future is valid if it's associated with a shared state. Default-constructed futures are not valid. A future that has had get() called on it is no longer valid. valid() allows a check before acting.

Checking valid() before calling get() is a defensive habit in code that passes futures around. If valid() returns false, get() throws std::future_error.

std::promise<T>: The Producer Side

std::promise<T> is the write end. It owns the shared state until either it fulfills the state or it's destroyed. The interface is tight: hand out the future once, then either set a value or set an exception.

set_value(): Fulfilling the Promise

set_value() stores the result in the shared state and unblocks any waiters. It can be called exactly once per promise.

Calling set_value() a second time on the same promise throws std::future_error with future_errc::promise_already_satisfied. The producer is contractually allowed one shot.

set_exception(): Failing the Promise

If the producer can't compute a value (the payment gateway returned an error, the customer service is unreachable, the discount code is invalid), it stores an exception instead. The consumer's get() rethrows that exception.

(Actual output reads Payment failed: Payment declined: amount exceeds limit; the message comes from the original throw site.) The pattern is: catch the exception in the producer, capture it with std::current_exception() into a std::exception_ptr, then hand the pointer to set_exception. On the consumer side, get() calls std::rethrow_exception internally, so a normal try/catch in the consumer thread is enough to handle it.

This is one of the headline features of futures and promises. Without them, propagating an exception across a thread boundary takes manual bookkeeping: store the exception_ptr somewhere, guard it with a mutex, wake the consumer, have the consumer re-throw. The promise handles all of that.

Promise Destroyed Without Setting a Value

If a promise is destroyed before either set_value or set_exception is called, the shared state is set to a std::future_error with the future_errc::broken_promise error condition. The consumer's get() throws this exception.

This is mostly a safety net. It saves the consumer from hanging forever when the producer thread crashes or returns early without fulfilling its promise. Treat a broken-promise exception as a bug somewhere in the producer logic, not as a normal flow.

set_value, set_exception, and get all touch the shared state's internal mutex. The cost is a single mutex acquire/release plus a condition variable signal in the typical implementation. It's measured in microseconds, not nanoseconds, so a million-promise-per-second pipeline needs a different design.

How promise.get_future() Connects the Two

The wiring is worth spelling out because it's the source of several common bugs. Default-constructing a std::promise<T> allocates a shared state, and the promise holds a pointer to it. get_future() returns a std::future<T> that also points to that same shared state. The result is two handles to one object.

get_future() can only be called once per promise. Calling it twice throws std::future_error with future_errc::future_already_retrieved. The standard enforces a one-to-one wiring between a promise and its primary future. (Conversion to a shared_future later allows many readers, but a second future cannot be pulled directly out of the promise.)

The shared state lives until both the promise and the future are destroyed (or moved-from). The standard library handles the reference counting internally with something like a std::shared_ptr to the state object. The lifetime is managed by the two endpoints, not by user code.

A subtle consequence: creating a promise on the stack and forgetting to move it into the thread destroys the promise at the end of the current scope, marks the shared state broken, and causes the future on the other end to throw broken_promise even when the worker thread is still running. The compiler usually catches this because std::promise is move-only, but accidental use of std::ref or capturing by reference in a lambda can produce dangling-reference bugs that compile but break at runtime.

Exception Propagation Across Threads

Exception propagation is the feature that sets futures and promises apart from raw shared variables. A realistic example: fetching customer details from a service that may fail.

Three things make this pattern work cleanly:

  1. The producer wraps its work in a single try { ... } catch (...) and uses std::current_exception() to capture whatever was thrown. This catches every exception type, including ones the producer didn't know about (a bad_alloc from inside a function it called, for example).
  2. result.set_exception(...) stores the std::exception_ptr in the shared state.
  3. The consumer's get() rethrows it, so the consumer's try/catch sees the original exception with its original type. Catching by base class (std::runtime_error), by derived class (std::invalid_argument), or by ... for everything are all options.

The type information survives the thread boundary. That's what raw shared memory can't easily provide.

Capturing an exception costs an allocation on most implementations (the exception_ptr is a shared_ptr to a heap-stored exception). Rethrowing costs an unwind. Avoid exceptions for normal flow on a hot path; reserve them for genuinely exceptional conditions.

std::shared_future<T>: Fan-Out to Multiple Consumers

A plain std::future can only be read once, and only one thread can hold it at a time. When several threads need to read the same result, convert the future to a std::shared_future. A shared_future is copyable, and every copy can call get() independently. They all see the same value (or the same exception).

The conversion is f.share(), which returns a shared_future and invalidates the original future. Alternatively, a shared_future can be constructed directly from a promise.get_future() result by passing the future to shared_future's constructor.

The exact order of the three cart lines depends on the scheduler. All three threads block on the same shared_future. When the discount lookup finishes and set_value(10.0) fires, all three wake and each one's get() returns 10.0.

shared_future is passed by value to each thread, because each thread needs its own copy of the handle. Internally, the copies share a reference-counted pointer to the same shared state, so there's no value duplication.

shared_future is the right pick for one-to-many fan-out of a single result. It adds a layer of reference counting on the shared state, so with only one consumer, future is the cheaper choice. Use shared_future only when there are actually multiple readers.

A common use case in e-commerce code: a product page load triggers a single product lookup, and several rendering threads (price formatting, recommendations sidebar, inventory badge) all need the same product record. One shared_future<Product> feeds all of them with no duplicated work.

Putting It All Together

A more complete example uses several of the pieces: a shipping-label generator running on a worker thread, a payment gateway whose result is fanned out to two consumer threads via shared_future, and exception handling on both pathways.

This is roughly the shape of a real checkout flow. Independent work runs in parallel. The single-shot label result goes through a plain future. The payment authorization fans out to two ledgers through a shared_future. Exceptions from either pathway are caught by the consumer that called get(). No mutexes, no condition variables, no manual flags.

To check "did this finish?" without consuming the value (useful for progress bars or speculative work), wait_for with a zero-length duration works: f.wait_for(std::chrono::seconds(0)) == std::future_status::ready is a non-blocking "is it done?" check.

When to Use What

A short decision table for choosing between the synchronization primitives covered so far.

NeedUse
Single producer, single consumer, one-shot valuestd::promise + std::future
Single producer, multiple consumers, one-shot valuestd::promise + std::shared_future
Multiple producers/consumers, ongoing communicationCondition variable + mutex (previous chapter)
Have the standard library start a taskstd::async (next chapter)
Wrap an existing function as a promisestd::packaged_task

std::async and std::packaged_task build on top of futures and promises. Both are higher-level conveniences that return a std::future without requiring a hand-built promise. They're covered in the next two chapters; for now, know they exist and that everything from this chapter applies to the futures they produce.

Quiz

Futures & Promises Quiz

10 quizzes