History of the question.
The std::future and std::promise facility arrived in C++11 to formalize asynchronous result transfer between threads. Earlier approaches relied on ad-hoc shared memory with manual synchronization, which made exception handling nearly impossible across thread boundaries. The standardization committee required a mechanism that could capture any exception type thrown in a worker thread and faithfully reproduce it in the waiting thread without knowing the exception's static type at the point of storage.
The problem.
Exception objects are polymorphic and stack-allocated by default, but they must survive the scope of the std::promise that produced them. Since std::future is templated only on the result type, not the exception type, the shared state cannot contain a typed exception member. Furthermore, the consumer thread may outlive the producer thread, requiring the exception to persist in heap-allocated storage with shared ownership semantics.
The solution.
The standard mandates that std::promise uses std::exception_ptr to capture exceptions via std::current_exception(), which performs implicit type erasure by copying the exception to the heap and storing a type-erased handle. The shared state (a reference-counted control block) retains this std::exception_ptr, allowing std::future::get() to detect the exception and rethrow it using std::rethrow_exception().
std::promise<int> prom; auto fut = prom.get_future(); std::thread([&prom]{ try { throw std::runtime_error("Worker failed"); } catch(...) { prom.set_exception(std::current_exception()); } }).detach(); try { int val = fut.get(); // Rethrows runtime_error } catch(const std::exception& e) { // Handles the transported exception }
Context.
A distributed computing framework required worker threads to process image segmentation tasks that could fail due to GPUOutOfMemory or CorruptInputData exceptions. The main thread needed to receive these specific exceptions to trigger fallback CPU processing or data retransmission.
Problem description.
Initial attempts used std::exception_ptr manually but suffered from lifetime bugs where exceptions were destroyed while still referenced by the main thread's error queue. Developers also struggled to store heterogeneous exception types in a single result container without slicing or object slicing during polymorphic storage.
Solution 1: Typed exception queues.
The team considered maintaining separate queues for each exception type using templates. This provided type safety but required std::any for type erasure in the common queue, adding significant overhead and complexity. It also broke the ability to catch exceptions naturally with try-catch blocks in the consumer thread.
Solution 2: Virtual exception holder.
They implemented an abstract ExceptionBase class with templated derived classes stored in std::unique_ptr<ExceptionBase>. While this enabled polymorphic storage, it required manual cloning logic to maintain shared ownership across threads and introduced virtual dispatch overhead during rethrowing. The custom reference counting was error-prone and difficult to make exception-safe itself.
Chosen solution and why.
The team adopted std::packaged_task with std::future, which internally uses the std::promise/std::exception_ptr mechanism. This eliminated custom type erasure code because the standard library handled the exception capture and shared state lifetime automatically. The choice was driven by the need for zero-maintenance exception safety and the requirement to support standard exception handling patterns without custom base classes.
Result.
The system successfully propagated specific exception types across thread boundaries with no memory leaks, even during aggressive thread pool resizing. The main thread could catch GPUOutOfMemory specifically while defaulting to std::exception for unknown errors, maintaining clean separation between error handling logic and thread synchronization.
Question: Why does std::current_exception() copy the exception object rather than storing a pointer to the existing exception?
Answer.
The exception object in a catch block is typically a temporary copy created by the runtime during stack unwinding. Storing a raw pointer would create a dangling reference once the catch block exits and the stack frame is destroyed. By copying the exception to the heap, std::current_exception() ensures the object persists independently of the throwing thread's stack. This copy operation also enables the type erasure mechanism, allowing std::exception_ptr to manage the object through a type-erased deleter while maintaining the ability to rethrow the exact original type later.
Question: How does std::promise prevent race conditions between set_value() and set_exception()?
Answer.
The shared state contains an atomic status flag tracking whether the promise is satisfied. When either set_value() or set_exception() is called, the implementation performs an atomic compare-and-swap operation to transition the state from "unsatisfied" to "ready". If the state is already ready, the operation throws std::future_error with promise_already_satisfied. This atomic transition ensures that the consumer thread observing the ready state sees a fully constructed value or exception, preventing partial reads or writes during concurrent access by the producer and consumer.
Question: Why can std::exception_ptr outlive both the std::promise and std::future that created it?
Answer.
std::exception_ptr uses intrusive reference counting on the exception object itself, independent of the std::future/std::promise shared state. This design allows exception handling code to store errors in long-lived logs or error handlers after the asynchronous operation has completed and its associated future/promise objects have been destroyed. The reference counting ensures the exception object is destroyed only when the last std::exception_ptr referencing it is destroyed, supporting use cases such as delayed error reporting or exception aggregation across multiple asynchronous operations.