Exploring futures and promises

C++
Author

Quasar

Published

November 17, 2025

Introduction

A future is an object that represents some undetermined result of a task that will be completed sometime in the future. A promise is the provider of that result.

The std::promise and std::future pair implements a one-short producer-consumer channel with the promise as the producer and the future as the consumer. The consumer (std::future) can can block until the result of the producer (std::promise) is available.

Many modern programming languages provide similar asynchronous approaches, such as Python(with the asyncio library), Scala(in the scala.concurrent library), Rust(in its standard library std or crates such as promising_future).

The basic principle behind achieving asynchronous execution using promises and futures is that a function we want to run to generate a result is executed in the background, using a new thread or the current one, and a future object is used by the initial thread to retrieve the result computed by the function. This result value will be stored when the function finishes, so meanwhile, the future acts as a placeholder. The asynchronous function will use a promise object to store the result in the future with no need for explicit synchronization mechanisms between the initial thread and the background one. When the value is needed by the initial thread, it will be retrieved from the future object. If the value is still not ready, the initial thread of execution will be blocked until the future becomes ready.

Using promises and futures improves responsiveness by offloading computations and provides a structured approach to handling asynchronous operations compared to threads and callbacks.

The Basic Mechanics

Promises

A promise holds a shared state. The shared state is a memory area that stores the completion status, synchronization mechanisms, and a pointer to the result. It ensures proper communication and synchronization between a promise and a future by enabling the promise to store either a result or an exception, signal when it’s complete and allowing the future to access the result, blocking if the promise is not yet ready. The promise can update its shared state using the following operations:

  • Make ready. The promise stores the result in the shared state and makes the state of the promise to become ready unblocking any thread waiting on a future associated with the promise.

  • Release. The promise releases its reference to the shared state, which will be destroyed if this is the last reference.

  • Abandon. The promise stores an exception of type std::future_error with error code std::future_errc::broken_promise making the shared state ready and then releasing it.

The value of a promise can be set using the std::promise function set_value() and an exception by using the set_exception() function. The result is stored atomically in the promise’s shared state, making its state ready. Let’s see an example:

// Ref: Asynchronous programming with C++
//Javier Reguara Salgado
auto threadFunc = [](std::promise<int> prom){
    try{
        int result = func();
        prom.set_value(result);
    }catch(std::exception& ex){
        prom.set_exception(ex);
    }
}

std::promise<int> prom;
std::jthread t(thread_func, std::move(prom));

set_value() can throw a std::future_error exception if the promise has no shared state (error code set to no_state) or the shared state has already a storedd result.

set_value() can also be used without specifying a value. In that case, it simply makes the state ready. That can be used as a barrier, as we will see later in this blog post.

Futures

Futures are defined in the <future> header file as std::future.

As we saw earlier, a future is the consumer side of the communication channel. It provides access to the result stored by the promise.

A std::future object must be created from std::promise object by calling get_future() or through a std::packaged_task object or a call to the std::async function.

std::promise<int> prom;
std::future<int> fut = prom.get_future();

Like promises, futures can be moved but not copied for the smae reasons. To reference the same shared state from multiple futures, we need to use shared futures.

The get() method can be used to retrieve the result. If the shared state is still not ready, this call will block internally calling wait(). When the shared state becomes ready, the result value is returned. If an exceptionwas sttored in the shared state, that exception will be retrhrown:

try{
    int result = fut.get()
    std::cout << "Result from thread:" << result;
}catch(const std::exception& ex){
    std::cerr << "Exception : " << ex.what() << "\n";
}

After calling the get() method, valid() will return false. If for some reason get() is called when valid() is false, the behavior is undefined, but the C++ standard recommends that a std::future_error exception is thrown with the std::future_errc::no_state error code.

When a future is destroyed, it releases it shared state reference. If that were the last reference, the shared state would be destroyed.

A quick working example

#include <algorithm>
#include <chrono>
#include <future>
#include <thread>
#include <vector>
#include <print>

using namespace std::chrono_literals;

int main(){
    std::promise<std::vector<int>> user_list_promise;
    std::promise<std::vector<int>> orders_promise;

    auto fetch_users_ready = user_list_promise.get_future();
    auto fetch_orders_ready = orders_promise.get_future();

    std::jthread user_list_thread([&](){
        std::vector<int> userList{};
        for(int i{1}; i<=5; ++i){
            userList.push_back(i);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
        user_list_promise.set_value(userList);
    });

    std::jthread orders_thread([&](){
        std::vector<int> orders{};
        for(int i{1}; i<=10; ++i){
            orders.push_back(i);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
        orders_promise.set_value(orders);
    });

    fetch_users_ready.wait();
    fetch_orders_ready.wait();

    auto users = fetch_users_ready.get();
    auto orders = fetch_orders_ready.get();

    std::println("Users = {}", users);
    std::println("Orders = {}", orders);
    return 0;
}

Compiler Explorer

Chaining asynchronous futures