Advanced 14 min

Futures and Promises

Learn to handle asynchronous results elegantly using futures and promises for cleaner concurrent code

Learn how to communicate results between threads elegantly using futures and promises, separating computation from result retrieval.

A Simple Example

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

int calculateSum(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return a + b;
}

void calculateAsync(std::promise<int> promise, int a, int b) {
    try {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        int result{a + b};
        promise.set_value(result);  // Deliver the result
    } catch (...) {
        promise.set_exception(std::current_exception());
    }
}

int main() {
    // Method 1: Using std::async (simpler)
    std::cout << "Starting async calculation...\n";
    std::future<int> future1{std::async(std::launch::async, calculateSum, 10, 20)};

    // Do other work while calculation runs
    std::cout << "Doing other work...\n";

    // Get the result (blocks if not ready)
    int result1{future1.get()};
    std::cout << "Result 1: " << result1 << "\n\n";

    // Method 2: Using promise and thread manually
    std::promise<int> promise;
    std::future<int> future2{promise.get_future()};

    std::thread worker{calculateAsync, std::move(promise), 30, 40};

    std::cout << "Waiting for result...\n";
    int result2{future2.get()};
    std::cout << "Result 2: " << result2 << "\n";

    worker.join();

    return 0;
}

Breaking It Down

std::future - The result placeholder

  • What it is: A placeholder for a value that will be available in the future
  • get(): Blocks until result is ready, then returns it (can only call once)
  • wait(): Blocks until result is ready without retrieving it
  • Remember: Calling get() consumes the future - you can only call it once

std::promise - The result setter

  • What it is: The producer side that delivers the result
  • set_value(): Stores the result and makes it available to the future
  • set_exception(): Propagates an exception to the future
  • Remember: You must set either value or exception, or the future will throw

Future states

  • valid(): Check if future has a shared state
  • wait_for(): Wait with timeout, returns status
  • future_status: ready, timeout, or deferred
  • Remember: Future becomes invalid after get() is called

Exception propagation

  • Automatic: Exceptions in async tasks are stored in the future
  • get() throws: When you call get(), any exception is re-thrown
  • Manual: Use set_exception() to explicitly store exceptions
  • Remember: Exceptions cross thread boundaries through futures

Why This Matters

  • Futures and promises provide a clean way to pass results from asynchronous operations back to the caller.
  • They separate the mechanism of starting async work from getting the result, making code more modular.
  • Built-in exception propagation means errors in worker threads are automatically delivered to the caller.

Critical Insight

A promise is like giving someone a ticket for a meal that is still cooking. The promise (kitchen) will eventually fulfill the order. The future (ticket) lets you wait for and claim the meal when ready.

The beauty is you can go do other things with your ticket (future) and only wait when you actually need the meal (call get()). If something goes wrong in the kitchen (exception), you find out when you try to claim your meal.

Best Practices

Use std::async when possible: It is simpler than manually creating promises and threads.

Always set value or exception: Never let a promise be destroyed without calling set_value() or set_exception().

Check valid() before get(): Ensure the future is valid before calling get() to avoid exceptions.

Use wait_for() for timeouts: Do not wait indefinitely - use wait_for() to detect slow operations.

Catch exceptions in async operations: Always wrap promise operations in try-catch to handle errors.

Common Mistakes

Calling get() twice: Future is consumed after get(). Use std::shared_future if you need multiple gets.

Forgetting to set promise: If you destroy a promise without setting it, the future throws an exception.

Blocking on get() too early: Calling get() immediately defeats the purpose of async. Do other work first.

Not handling exceptions: Exceptions in async tasks are propagated. Always be prepared to catch them.

Debug Challenge

This code tries to use the future twice, which is invalid. Click the highlighted line to fix it:

1 std::promise<int> promise;
2 std::future<int> future{promise.get_future()};
3
4 std::thread worker{[&promise]() {
5 promise.set_value(42);
6 }};
7
8 int result1{future.get()};
9 int result2{future.get()};

Quick Quiz

  1. How many times can you call get() on a std::future?
Once - it consumes the future
Unlimited times
Twice
  1. What happens if you destroy a promise without setting a value?
The future waits forever
The future throws std::future_error when you call get()
Nothing, it is fine
  1. How do exceptions cross thread boundaries with futures?
They crash the program
They are silently ignored
They are stored in the future and re-thrown when get() is called

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once