asynchronous programming
error handling
concurrency
Future vs Promise
software development

futures, promises, and exceptions

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Futures and promises are two sides of an asynchronous result. One side produces the value later, and the other side waits for or observes that result. Exceptions matter because asynchronous work can fail just as normal function calls can fail, but the failure has to cross a time boundary and often a thread boundary.

Promise as Producer, Future as Consumer

The cleanest mental model is:

  • a promise is completed by the producer
  • a future is observed by the consumer

The producer says, "I will provide a result later." The consumer says, "Tell me when that result or error is ready."

C++ makes this split explicit.

cpp
1#include <future>
2#include <iostream>
3#include <thread>
4
5int main() {
6    std::promise<int> p;
7    std::future<int> f = p.get_future();
8
9    std::thread worker([p = std::move(p)]() mutable {
10        p.set_value(42);
11    });
12
13    std::cout << f.get() << "
14";
15    worker.join();
16}

The worker owns the writable side. The caller owns the readable side.

Why Exceptions Need a Delivery Path

In synchronous code, a function can throw directly to its caller because both exist on the same call stack. In asynchronous code, the computation may finish later on another thread, so the exception cannot be thrown immediately to the original caller.

Instead, the failure must be stored until the consumer asks for the result.

cpp
1#include <future>
2#include <iostream>
3#include <stdexcept>
4#include <thread>
5
6int main() {
7    std::promise<int> p;
8    std::future<int> f = p.get_future();
9
10    std::thread worker([p = std::move(p)]() mutable {
11        try {
12            throw std::runtime_error("database timeout");
13        } catch (...) {
14            p.set_exception(std::current_exception());
15        }
16    });
17
18    try {
19        std::cout << f.get() << "
20";
21    } catch (const std::exception& ex) {
22        std::cout << "Caught: " << ex.what() << "
23";
24    }
25
26    worker.join();
27}

The exception is rethrown when get is called. That keeps success and failure on the same asynchronous channel.

Futures Reduce Shared-State Coordination

Without a future or similar abstraction, developers often end up building ad hoc state sharing with:

  • flags
  • mutexes
  • condition variables
  • polling loops

A future simplifies that into one clear contract: one eventual outcome.

That makes futures a natural fit for:

  • background computation
  • thread-pool tasks
  • RPC results
  • long-running I/O completion

They are not a substitute for every concurrency pattern, but they are a strong fit when the result is single-shot.

Broken Promises and Missing Completion

A promise must be completed exactly once with either:

  • a value
  • an exception

If the promise is abandoned without either one, the future usually reports some form of broken-promise error. That is not just an API quirk. It signals that the producer violated the async contract.

This is why robust promise code always handles the failure path explicitly.

Relationship to Other Languages

The terminology varies, but the concept is widespread.

Examples:

  • JavaScript Promise combines producer and consumer API behavior in one object.
  • C# often uses Task for the readable side and TaskCompletionSource for manual completion.
  • Scala and some other languages expose Future and Promise as separate objects, much like C++.

The naming is different, but the exception rule is stable: async failure should travel with the async result.

Common Pitfalls

  • Forgetting to propagate exceptions into the promise, which leaves the consumer without the true failure cause.
  • Thinking futures are general shared-state containers instead of single eventual results.
  • Calling get without knowing whether the future type is single-consumer or shareable.
  • Overusing manual promises where a higher-level async API already handles completion for you.
  • Treating asynchronous exceptions as second-class errors instead of part of the normal result contract.

Summary

  • A promise is the writable side of an eventual result, and a future is the readable side.
  • Exceptions in asynchronous work must be captured and delivered through the future.
  • Futures are useful when one producer eventually yields one success or one failure.
  • Abandoning a promise without completion breaks the async contract.
  • Good async design treats failure propagation as just as important as value propagation.

Course illustration
Course illustration

All Rights Reserved.