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.

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:

  1. Inside the producer thread, catch any exception with catch (...).
  2. Capture it with std::current_exception(), which returns a non-null std::exception_ptr.
  3. Store it via promise::set_exception(ptr) — this atomically marks the shared state ready.
  4. On the consumer side, future::get() detects that the shared state holds an exception and rethrows it with its original type intact.
cpp
#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:

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

cpp
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 false

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

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

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

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

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

Criterionstd::asyncstd::packaged_taskstd::promise
Who drives executionruntime (may use thread pool)you schedule it explicitlyyou fulfill it manually
Wraps an existing callableyes, directlyyes, wraps itno
Can fulfill from non-callable contextsnonoyes
Overheadhighest (may launch thread)medium (one allocation)one allocation
Boilerplateminimalmoderatemost
C++ versionC++11C++11C++11

Use std::async when: you have a free function or lambda and just want the result asynchronously. It handles threading and fulfillment automatically.

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

cpp
std::packaged_task<int(int)> task(compute);
auto f = task.get_future();
thread_pool.push(std::move(task));   // task() calls compute and fulfills f

Use 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.

cpp
// 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_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