C++ std::async, std::future and std::promise Explained
A C++ future is a one-shot channel for a value or an exception. std::async launch policies, promise and packaged task, and the blocking destructor trap.
- Course: C++ study plan
- Module: Concurrency
- Kind: Lesson
- Reading time: 15 min
- Runtime: Clang 18 · C++20
How do std::async and std::future work in C++?
std::async runs a callable — on a new thread when given std::launch::async — and returns a std::future for its result. Calling get() on the future waits until the value exists, then moves it out, or rethrows the exception the task threw. A future can be read exactly once; share() makes a std::shared_future for several readers. Futures are how C++ hands results and errors back from another thread.
Lesson
A thread that computes a value has nowhere to put it: std::thread returns nothing, and an exception inside its body terminates the whole program. The <future> header fixes both with one idea. A future is a one-shot channel that will hold either a value or an exception; whoever produces it is elsewhere, and get() waits for it and hands it over — or rethrows. This lesson covers the three producers of a future — std::async, std::promise and std::packaged_task — the launch policies and the trap in std::async's destructor, how an exception crosses a thread boundary, and a parallel sum that collects its partial results in a fixed order.
std::async and std::future
#include <future>
#include <iostream>
long long sum_to(long long n) {
long long s = 0;
for (long long i = 1; i <= n; ++i) s += i;
return s;
}
int main() {
std::future<long long> f = std::async(std::launch::async, sum_to, 1'000'000);
// … other work here runs in parallel with sum_to …
std::cout << f.get() << '\n'; // waits if needed: 500000500000
return 0;
}
std::async takes a callable and its arguments the way std::thread does — copied, std::ref for references — and returns a std::future<R> for the callable's return type. get() blocks until the result exists, then moves it out; a future can be get() exactly once, and valid() is false afterwards (a second get() throws std::future_error). wait() blocks without taking the value; wait_for(duration) returns a std::future_status — ready, timeout or deferred. When several threads need the same result, f.share() turns it into a std::shared_future, which may be copied and read many times.
Launch policies
The first argument decides where the work runs. std::launch::async starts a new thread now. std::launch::deferred runs the function lazily, on the thread that calls get(), and not at all if nobody does — a lazy value, not concurrency. The default, std::launch::async | std::launch::deferred, lets the implementation choose; libstdc++ picks a thread, but code that needs parallelism should say so: always write std::launch::async when the point is to run in parallel.
The trap is the destructor. A future obtained from std::async — and only that kind — blocks in its destructor until the task finishes. So this loop is sequential:
for (int i = 0; i < 4; ++i)
std::async(std::launch::async, work, i); // temporary future destroyed at once: waits
Each temporary waits for its task before the next starts. Keep the futures — std::vector<std::future<int>> — and the tasks overlap.
Exceptions cross threads through the future
If the callable throws, the exception is caught by the library, stored in the shared state, and rethrown by get() on whichever thread calls it. That is the whole mechanism for error handling across threads, and it is the only one: an exception that escapes a plain std::thread body calls std::terminate.
int parse_positive(const std::string& s) {
const int v = std::stoi(s); // may throw std::invalid_argument
if (v <= 0) throw std::out_of_range("not positive: " + s);
return v;
}
auto f = std::async(std::launch::async, parse_positive, std::string("-3"));
try {
std::cout << f.get() << '\n';
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n'; // error: not positive: -3
}
Catch by const& at the get(), as in Module 15; the dynamic type is preserved, so a std::out_of_range handler matches only that.
std::promise: setting the value by hand
std::async produces the value by returning it. A std::promise<T> lets any code, on any thread, put a value into a future explicitly — once:
#include <future>
#include <thread>
void find_first(const std::vector<int>& v, int target, std::promise<int> result) {
for (std::size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) { result.set_value(static_cast<int>(i)); return; }
}
result.set_exception(std::make_exception_ptr(std::runtime_error("not found")));
}
std::promise<int> p;
std::future<int> f = p.get_future(); // get_future() once, before moving p
std::thread worker(find_first, std::cref(v), 42, std::move(p));
const int index = f.get(); // or throws runtime_error
worker.join();
set_value or set_exception is called exactly once; a second call throws std::future_error (promise_already_satisfied). A promise destroyed without either call stores a std::future_error with broken_promise, so the waiter does not hang. std::promise<void> carries no value and is a one-shot signal — "the worker has initialised". Inside a catch block, std::current_exception() captures whatever was thrown, ready for set_exception. Promises are move-only, so they travel into the thread with std::move.
std::packaged_task: a callable with a future attached
A std::packaged_task<R(Args...)> wraps a callable; calling the task runs it and stores the return value or exception in a future obtained beforehand:
std::packaged_task<long long(long long)> task(sum_to);
std::future<long long> f = task.get_future();
std::thread t(std::move(task), 1000); // run it on another thread …
t.join();
std::cout << f.get() << '\n'; // 500500
The task can be called on any thread, later, by anyone who holds it — which is exactly what a thread pool needs: the pool stores tasks in a queue, a worker calls them, and the submitter already holds the future (lesson 6). Like promises, packaged tasks are move-only.
A parallel sum, joined deterministically
std::vector<std::future<long long>> parts;
for (std::size_t i = 0; i < t; ++i) {
const std::size_t lo = i * v.size() / t, hi = (i + 1) * v.size() / t;
parts.push_back(std::async(std::launch::async, [&v, lo, hi] {
long long s = 0;
for (std::size_t k = lo; k < hi; ++k) s += v[k];
return s;
}));
}
long long total = 0;
for (auto& p : parts) total += p.get(); // index order, whatever finished first
The threads finish in whatever order the scheduler decides; the get() loop consumes them in index order, so the merge is the same on every run — and parts[i].get() is the per-thread partial, printable as "thread i". A future per chunk (not per element) keeps thread creation cheap, and the vector of futures does the joining, since each get() waits.
Which one when
| Tool | Who produces the value | Typical use |
|---|---|---|
std::async | The function's return | Run a function in parallel and collect its result |
std::packaged_task | The wrapped callable, when someone calls the task | Queued work: thread pools |
std::promise | Explicit set_value/set_exception | Hand-built protocols, a signal, a result produced mid-function |
std::thread + std::ref | You write into a slot | Fire-and-join workers with no error path |
Pitfalls
| Mistake | What happens |
|---|---|
Discarding the future from std::async | Its destructor waits; the loop runs sequentially |
| Omitting the launch policy and expecting a thread | Allowed to be deferred; runs at get() |
get() twice | std::future_error; use std::shared_future for several readers |
| A promise destroyed without a value | The waiter gets broken_promise |
Calling get_future() after moving the promise | The moved-from promise has no shared state |
Throwing inside a std::thread body | No future to carry it — std::terminate |
Key takeaways
- A future is a one-shot channel for a value or an exception;
get()waits, moves the value out, and rethrows what the producer threw. std::async(std::launch::async, f, args…)runsfon a new thread; name the policy, and keep the future — anstd::asyncfuture's destructor blocks.std::promisesets a result by hand (set_value/set_exception, exactly once);std::packaged_taskattaches a future to any callable.- Exceptions cross threads only through futures; an exception escaping a
std::threadterminates the program. - Collect futures in a vector and
get()them in index order: deterministic merging whatever finished first.
Common questions
What is the difference between std::launch::async and std::launch::deferred?
std::launch::async starts a new thread immediately. std::launch::deferred runs the function lazily on the thread that calls get() or wait(), and never if nobody does. The default policy allows either, so write std::launch::async whenever the point is parallelism.
Why does std::async run sequentially in a loop?
A future returned by std::async blocks in its destructor until the task finishes. Calling std::async without keeping the result creates a temporary future that is destroyed at the end of the statement, so each iteration waits for its own task. Store the futures in a vector and the tasks overlap.
How are exceptions passed between threads in C++?
Through a future. If a task run by std::async or a std::packaged_task throws, the library stores the exception and get() rethrows it on the calling thread with its dynamic type intact; a std::promise stores one with set_exception. An exception that escapes a plain std::thread body calls std::terminate.
What is the difference between std::promise and std::packaged_task?
A std::promise lets any code set a future's result by hand, exactly once, with set_value or set_exception. A std::packaged_task wraps a callable and fills its future with the return value or exception when the task is called — the building block of thread pools. Both are move-only.
What is a broken promise error in C++?
When a std::promise is destroyed before set_value or set_exception was called, its future receives a std::future_error with the code broken_promise, so a thread waiting in get() fails with an exception instead of hanging forever.
Exercises
Digit sums through std::async
Read t and n, then n non-negative integers. Split the indices into t contiguous chunks and launch each with std::async(std::launch::async, ...) — name the policy, so the work really runs in parallel — computing the sum of the digit sums of the values in the chunk (305 contributes 3 + 0 + 5 = 8). Keep every std::future<long long> in a vector; a future from std::async blocks in its destructor, so discarding it would make the loop sequential. Then get() them in index order, whatever order they finished, printing chunk <i>: <sum> and finally total <sum>.
Input: t n, then n integers. Output: t + 1 lines.
2 5
12 305 7 99 1000
→
chunk 0: 11
chunk 1: 26
total 37A promise kept or broken
Write void find_first(const std::vector<int>& v, int target, std::promise<int> result), which searches v for target and either set_values the first index or set_exceptions a std::runtime_error("not found") — exactly one of the two, once. Read n values, then q targets. For each target create a std::promise<int>, take its future before moving the promise into a std::thread, and keep the futures and the threads in vectors. Then, in query order, get() each future inside a try: print <target> found at <index> or, on std::runtime_error, <target> not found. Join every thread before returning.
Input: n, a line of n integers, q, a line of q targets. Output: q lines.
5
4 8 15 16 23
3
15 4 42
→
15 found at 2
4 found at 0
42 not foundIn this module: Concurrency
- Threads — std::thread, std::jthread and the join rule
- Mutexes and locks — making shared writes safe
- Atomics — lock-free counters and compare-exchange
- Condition variables — waiting without spinning
- Futures, promises and std::async — results that arrive later (this lesson)
- Concurrency patterns — pools, reductions and the deterministic-output rule
- Checkpoint — Concurrency
← Condition variables — waiting without spinning · Concurrency patterns — pools, reductions and the deterministic-output rule →