Using std::promise for Asynchronous Results in C++

This article provides an overview of std::promise in C++, explaining its fundamental role in multithreaded programming. It explores how std::promise acts as the producer endpoint in asynchronous communication, how it pairs with std::future to create a synchronization channel, and how it enables threads to pass both computed values and unhandled exceptions safely across thread boundaries without manual locking primitives.

The Core Role of std::promise

In C++ concurrency, std::promise provides a mechanism to store a value or an exception that is later acquired asynchronously via a std::future object. Together, a std::promise and a std::future form a single-use communication channel:

  • std::promise (The Producer): Owned by the thread that produces the result. It writes data to the shared state.
  • std::future (The Consumer): Owned by the thread waiting for the result. It reads data from the shared state.

While high-level utilities like std::async automatically manage this relationship behind the scenes, std::promise gives developers explicit control over when, where, and how a result is made available.

Explicit and Decoupled Synchronization

The primary advantage of std::promise is decoupling result production from thread execution. A thread does not need to terminate to yield its result. A worker thread can initialize a calculation, satisfy the promise midway through its execution by calling set_value(), and then continue performing other non-dependent operations.

#include <iostream>
#include <thread>
#include <future>

void computeValue(std::promise<int> resultPromise) {
    // Perform intensive task
    int result = 42; 

    // Make the result available to the waiting thread immediately
    resultPromise.set_value(result);

    // The thread can continue running background cleanup or logging
}

int main() {
    std::promise<int> myPromise;
    std::future<int> myFuture = myPromise.get_future();

    std::thread worker(computeValue, std::move(myPromise));

    // Blocks until resultPromise.set_value() is called
    std::cout << "Result: " << myFuture.get() << std::endl;

    worker.join();
    return 0;
}

Exception Propagation Across Threads

Standard C++ threads cannot naturally throw exceptions across execution contexts; an uncaught exception on a child thread typically triggers std::terminate.

std::promise solves this problem with the set_exception() method. By capturing an active exception pointer via std::current_exception(), a worker thread can store the failure inside the promise. When the consumer thread calls .get() on its associated std::future, the stored exception is re-thrown in the context of the consumer thread, allowing standard try-catch exception handling:

void riskyTask(std::promise<int> promise) {
    try {
        throw std::runtime_error("Calculation failed");
    } catch (...) {
        promise.set_exception(std::current_exception());
    }
}

Replacing Low-Level Primitives

Before the introduction of std::promise in C++11, passing a single result between threads required combining a shared variable, a std::mutex, and a std::condition_variable.

Using std::promise simplifies this pattern significantly:

  1. No Manual Locking: Thread synchronization and memory visibility barriers are handled internally.
  2. Safety Guarantees: A promise can only be set once. Attempting to set a value or exception multiple times automatically throws a std::future_error with the error code promise_already_satisfied.
  3. Broken Promise Detection: If a std::promise is destroyed before setting a value, its associated std::future will throw a std::future_error with broken_promise, preventing the consumer thread from blocking indefinitely.