Skip to content
C++
Library
since C++11
Intermediate

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++11

A 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:

  1. Construct std::promise<T> on the producing side.
  2. Extract std::future<T> via get_future() and pass it to the consumer.
  3. Move the promise into the producer thread or callable.
  4. Call set_value() or set_exception() exactly once.
  5. Consumer calls future::get(), blocking until the state is ready.

Promise vs. packaged_task vs. async

MechanismWho controls fulfillmentBest for
std::promiseyou, manuallycustom executors, event-driven code, manual signaling
std::packaged_taskwraps a callableadapting existing functions to return futures
std::asyncthe runtimesimple fire-and-forget or straightforward async calls

Syntax

cpp
#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 exception

Member function summary:

MemberDescription
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

cpp
#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.

cpp
#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.

cpp
#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:

cpp
#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.

See Also

  • std::future / std::shared_future — read endpoints of the shared state; shared_future allows multiple consumers
  • std::packaged_task — wraps any callable and exposes a future without manual promise management
  • std::async — highest-level abstraction; appropriate for most single-result async patterns
  • std::condition_variable — lower-level notification primitive; more control at the cost of explicit mutex management