std::promise
Write-end of a one-shot, thread-safe channel; pairs with std::future to pass a value or exception across threads.
std::promise<T>since C++11A move-only, one-time write endpoint that stores a value or exception into a heap-allocated shared state retrievable by the corresponding std::future<T>.
Overview
std::promise<T> and std::future<T> form a single-producer, single-consumer rendezvous. The promise owns the write side; get_future() hands off the read side. Once the promise fulfills the shared state — either with a value via set_value() or with an exception via set_exception() — any thread blocking on future::get() unblocks and retrieves the result.
The shared state is heap-allocated and reference-counted between both endpoints. A std::promise is move-only; copying it is deleted. Each promise can produce exactly one future: a second call to get_future() throws std::future_error with std::future_errc::future_already_retrieved. Both types live in <future> (C++11).
The standard workflow:
- Construct
std::promise<T>on the producing side. - Extract
std::future<T>viaget_future()and pass it to the consumer. - Move the promise into the producer thread or callable.
- Call
set_value()orset_exception()exactly once. - Consumer calls
future::get(), blocking until the state is ready.
Promise vs. packaged_task vs. async
| Mechanism | Who controls fulfillment | Best for |
|---|---|---|
std::promise | you, manually | custom executors, event-driven code, manual signaling |
std::packaged_task | wraps a callable | adapting existing functions to return futures |
std::async | the runtime | simple fire-and-forget or straightforward async calls |
Syntax
#include <future>
std::promise<T> p; // primary template; T must be MoveConstructible
std::promise<T&> p; // reference specialisation
std::promise<void> p; // signaling with no payload
std::future<T> f = p.get_future(); // call at most once
p.set_value(v); // fulfil with a value
p.set_exception(eptr); // fulfil with an exceptionMember function summary:
| Member | Description |
|---|---|
get_future() | Returns the associated future; throws on second call |
set_value(v) | Stores value; unblocks future::get() |
set_exception(eptr) | Stores exception; future::get() rethrows it |
set_value_at_thread_exit(v) | Defers fulfillment until current thread exits |
set_exception_at_thread_exit(eptr) | Defers exception storage until thread exits |
The _at_thread_exit variants ensure thread-local destructors finish before the consumer observes the result.
Examples
Basic producer–consumer handoff
#include <future>
#include <numeric>
#include <thread>
#include <vector>
#include <iostream>
int main() {
std::promise<long long> sum_promise;
std::future<long long> sum_future = sum_promise.get_future();
std::thread worker([p = std::move(sum_promise)]() mutable {
std::vector<int> data(100'000);
std::iota(data.begin(), data.end(), 1);
long long result = std::accumulate(data.begin(), data.end(), 0LL);
p.set_value(result);
});
std::cout << "Sum: " << sum_future.get() << '\n'; // blocks until ready
worker.join();
}Exception propagation
set_exception stores any exception pointer; future::get() rethrows it on the consumer's thread with the original type intact.
#include <future>
#include <thread>
#include <stdexcept>
#include <iostream>
void safe_divide(std::promise<double> p, double num, double den) {
try {
if (den == 0.0) throw std::domain_error("division by zero");
p.set_value(num / den);
} catch (...) {
p.set_exception(std::current_exception());
}
}
int main() {
std::promise<double> p;
auto f = p.get_future();
std::thread t(safe_divide, std::move(p), 10.0, 0.0);
try {
std::cout << f.get() << '\n';
} catch (const std::domain_error& e) {
std::cerr << "Caught: " << e.what() << '\n'; // "Caught: division by zero"
}
t.join();
}Void promise as a one-shot signal
std::promise<void> carries no payload — it functions as a lightweight notification primitive, replacing a condition_variable + bool flag with less ceremony.
#include <future>
#include <thread>
#include <iostream>
void do_work(std::future<void> ready) {
ready.wait(); // block until signaled
std::cout << "Worker running.\n";
}
int main() {
std::promise<void> gate;
auto f = gate.get_future();
std::thread worker(do_work, std::move(f));
// ... perform setup ...
std::cout << "Setup complete.\n";
gate.set_value(); // no argument for void specialisation
worker.join();
}Thread-pool task submission
std::promise is the right primitive when fulfillment is decoupled from a single callable — for example, in a hand-rolled executor:
#include <functional>
#include <future>
#include <mutex>
#include <queue>
#include <thread>
#include <type_traits>
struct Pool {
std::queue<std::function<void()>> tasks;
std::mutex mu;
std::vector<std::thread> workers;
template <typename F, typename R = std::invoke_result_t<F>> // C++17
std::future<R> submit(F f) {
auto p = std::make_shared<std::promise<R>>();
auto fut = p->get_future();
{
std::lock_guard lk(mu); // C++17: CTAD
tasks.push([p, f = std::move(f)]() mutable {
try {
p->set_value(f());
} catch (...) {
p->set_exception(std::current_exception());
}
});
}
return fut;
}
};Best Practices
Move the promise; never share it. Promises are move-only for a reason. Pass ownership with std::move into the thread or lambda at construction time. Passing by reference invites lifetime hazards if the owning thread exits before fulfillment.
Always fulfill. If a promise is destroyed without set_value or set_exception, its destructor writes a std::future_error with std::future_errc::broken_promise into the shared state. The consumer's future::get() then throws silently corrupting your application logic. Gate all exit paths — normal returns, early returns, and exceptions — so exactly one fulfillment call occurs.
Prefer std::async for simple cases. std::async wires up promise and future automatically and handles the thread lifetime. Reach for std::promise only when fulfillment is genuinely decoupled from the callable that does the computation — custom schedulers, coroutine adapters, or event loops.
Use set_value_at_thread_exit for thread-local cleanup ordering. When the producing thread owns thread-local objects whose destructors must complete before the consumer proceeds, the _at_thread_exit variant guarantees that ordering without extra synchronization.
Fan out with std::shared_future. std::promise maps one-to-one with one future. When multiple threads must observe the same result, call future::share() to obtain a std::shared_future<T> that can be copied freely and get()-d from any number of threads.
Common Pitfalls
Calling get_future() twice. Each promise yields exactly one future. A second call throws std::future_error(future_errc::future_already_retrieved). Obtain the future before moving the promise, and hold only one copy.
Setting the value more than once. set_value and set_exception are one-shot. A second call throws std::future_error(future_errc::promise_already_satisfied). Ensure a single code path reaches the set call, or guard it with an atomic flag.
Capturing the promise by reference in a lambda. Passing a std::promise& to std::thread keeps ownership on the launching thread. If it exits before the worker calls set_value, the promise destructs first and the future receives broken_promise. Always capture by move: [p = std::move(promise)]() mutable { ... }.
Heap allocation cost. The shared state is heap-allocated per promise. In tight dispatch loops — millions of short tasks per second — this becomes measurable. Consider pooling promises, using lock-free queues, or C++20 coroutines (co_await) where allocation amortization matters.
Exception Propagation in Depth
The promise–future pair creates a type-safe exception channel across thread boundaries. The mechanism relies on std::exception_ptr, a shared-ownership handle to a heap-allocated exception object.
How it works:
- Inside the producer thread, catch any exception with
catch (...). - Capture it with
std::current_exception(), which returns a non-nullstd::exception_ptr. - Store it via
promise::set_exception(ptr)— this atomically marks the shared state ready. - On the consumer side,
future::get()detects that the shared state holds an exception and rethrows it with its original type intact.
#include <future>
#include <stdexcept>
#include <iostream>
#include <thread>
// Worker that may throw any exception type
std::string parse_config(const std::string& path) {
if (path.empty())
throw std::invalid_argument("empty path");
return "config-data";
}
int main() {
std::promise<std::string> p;
auto f = p.get_future();
std::thread t([&p] {
try {
p.set_value(parse_config("")); // throws
} catch (...) {
p.set_exception(std::current_exception()); // capture and forward
}
});
try {
std::string result = f.get(); // rethrows std::invalid_argument
std::cout << result << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "Config error: " << e.what() << '\n';
}
t.join();
}You can also inject a specific exception without catching one in flight using std::make_exception_ptr:
p.set_exception(
std::make_exception_ptr(std::runtime_error("connection timeout"))
);This avoids the throw–catch round-trip when you know the error at construction time.
Key guarantee: set_exception is atomic with respect to set_value and other set_exception calls on the same promise. Concurrent calls to get_future are also safe. However, calling set_exception a second time throws std::future_error with future_errc::promise_already_satisfied — the same restriction as set_value.
Passing nullptr is undefined behavior. Always ensure the exception_ptr is non-null before passing it to set_exception. std::current_exception() inside a catch block is always non-null; std::make_exception_ptr always returns non-null.
std::shared_future: Multiple Consumers
std::future is move-only and its shared state is invalidated after get() is called. When multiple threads need to observe the same result, use std::shared_future<T> instead.
Converting a future to a shared_future:
std::promise<int> p;
std::future<int> f = p.get_future();
// Option A: implicit conversion
std::shared_future<int> sf = p.get_future();
// Option B: explicit share() — invalidates the original future
std::shared_future<int> sf = f.share(); // f.valid() is now falseAfter share(), the original std::future is empty (valid() returns false). The shared_future can be copied freely — each copy refers to the same shared state.
Multiple threads waiting on the same result:
#include <future>
#include <thread>
#include <vector>
#include <iostream>
int main() {
std::promise<int> p;
std::shared_future<int> sf = p.get_future().share();
// Launch several consumers, each holding their own copy
std::vector<std::thread> consumers;
for (int i = 0; i < 4; ++i) {
consumers.emplace_back([sf, i] { // copy sf per thread — safe
int val = sf.get();
std::cout << "Consumer " << i << " got: " << val << '\n';
});
}
p.set_value(42); // all four consumers unblock
for (auto& t : consumers) t.join();
}Thread-safety rule: Each thread must access the shared state through its own copy of the shared_future. Sharing a single shared_future object across threads and calling get() concurrently is a data race. Copying the object is the prescribed pattern.
get() does not invalidate the state. Unlike std::future::get(), calling shared_future::get() can be repeated on the same instance and from multiple threads (each through its own copy). The value is stored persistently in the shared state until all shared_future copies are destroyed.
wait_for and wait_until work identically on shared_future:
using namespace std::chrono_literals;
if (sf.wait_for(100ms) == std::future_status::ready) {
std::cout << sf.get() << '\n';
}Typical use cases for shared_future:
- Configuration or initialization results that many worker threads read at startup.
- A "go" signal broadcast to a pool of waiting threads (use
shared_future<void>). - Cached computation results shared across components without external synchronization.
Broken Promise: When the Producer Disappears
When a std::promise is destroyed without having called set_value or set_exception, its destructor performs the abandon operation: it stores a std::future_error with error code std::future_errc::broken_promise into the shared state and marks it ready. Any thread blocking on future::get() then unblocks and the get() call throws.
#include <future>
#include <iostream>
int main() {
std::future<int> f;
{
std::promise<int> p;
f = p.get_future();
// p goes out of scope without set_value or set_exception
} // p.~promise() → broken_promise stored in shared state
try {
f.get(); // throws
} catch (const std::future_error& e) {
// e.code() == std::future_errc::broken_promise
std::cerr << "Broken promise: " << e.what() << '\n';
}
}Common causes:
- Promise moved into a thread that exits via an uncaught exception before fulfilling.
- Promise captured in a lambda that returns early without setting the value.
- Promise moved to a queue that is drained and the task is dropped rather than executed.
std::promiseis move-assigned — the old promise's state is abandoned.
Preventing broken promises:
The most reliable pattern is a RAII guard that fulfills the promise on any exit path:
template <typename T>
struct PromiseGuard {
std::promise<T>& promise;
bool fulfilled = false;
~PromiseGuard() {
if (!fulfilled) {
try {
promise.set_exception(
std::make_exception_ptr(
std::runtime_error("operation cancelled")));
} catch (...) { } // already satisfied is ok
}
}
};Alternatively, structure the worker so that set_value / set_exception is called inside a try/catch (...) block that always reaches one of the two, as shown in the exception propagation example above.
Decision Guide: promise vs. packaged_task vs. async
All three primitives use the same shared-state machinery under the hood. The choice is about who controls fulfillment and how much boilerplate you accept.
| Criterion | std::async | std::packaged_task | std::promise |
|---|---|---|---|
| Who drives execution | runtime (may use thread pool) | you schedule it explicitly | you fulfill it manually |
| Wraps an existing callable | yes, directly | yes, wraps it | no |
| Can fulfill from non-callable contexts | no | no | yes |
| Overhead | highest (may launch thread) | medium (one allocation) | one allocation |
| Boilerplate | minimal | moderate | most |
| C++ version | C++11 | C++11 | C++11 |
Use std::async when: you have a free function or lambda and just want the result asynchronously. It handles threading and fulfillment automatically.
auto f = std::async(std::launch::async, compute, args...);
int result = f.get();Use std::packaged_task when: you want to decouple wrapping a callable from scheduling it — for example, pushing work into a queue that a thread pool drains.
std::packaged_task<int(int)> task(compute);
auto f = task.get_future();
thread_pool.push(std::move(task)); // task() calls compute and fulfills fUse std::promise when: fulfillment is entirely decoupled from a single callable invocation — custom executors, event loops, coroutine adapters, I/O completion callbacks, or any situation where the result is produced by code that was not written as a callable to be wrapped.
// Event loop pattern: I/O completion triggers fulfillment
void on_read_complete(Buffer buf, std::promise<Buffer> p) {
p.set_value(std::move(buf)); // called from event loop, not a task
}As a rule of thumb: reach for async first, then packaged_task if you need scheduling control, then promise only when the computation cannot be expressed as a single callable.
See Also
std::future/std::shared_future— read endpoints of the shared state;shared_futureallows multiple consumersstd::packaged_task— wraps any callable and exposes a future without manual promise managementstd::async— highest-level abstraction; appropriate for most single-result async patternsstd::condition_variable— lower-level notification primitive; more control at the cost of explicit mutex management