multithreading
exception handling
thread communication
concurrency
programming tips

Catch a thread's exception in the caller thread?

Master System Design with Codemia

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

Introduction

An exception thrown inside a worker thread does not jump across to the thread that launched it. Exception handling follows call stacks, and each thread has its own stack. If you want the caller thread to observe a worker failure, you need to capture the exception in the worker and transport it explicitly.

Why a Caller-Side try Block Is Not Enough

This is the core misconception:

cpp
1try {
2    std::thread t([] {
3        throw std::runtime_error("boom");
4    });
5    t.join();
6} catch (...) {
7    // this does not catch the worker exception
8}

The catch block is on the caller thread's stack, not the worker's stack. An uncaught exception in a raw std::thread typically ends the program through std::terminate.

So the correct model is:

  1. catch inside the worker
  2. store the failure somewhere shared
  3. rethrow or inspect it after join

Use std::exception_ptr with Raw Threads

The standard transport type in C++ is std::exception_ptr.

cpp
1#include <exception>
2#include <iostream>
3#include <stdexcept>
4#include <thread>
5
6int main() {
7    std::exception_ptr worker_error;
8
9    std::thread worker([&worker_error] {
10        try {
11            throw std::runtime_error("worker failed");
12        } catch (...) {
13            worker_error = std::current_exception();
14        }
15    });
16
17    worker.join();
18
19    try {
20        if (worker_error) {
21            std::rethrow_exception(worker_error);
22        }
23        std::cout << "worker completed successfully\n";
24    } catch (const std::exception& ex) {
25        std::cout << "caller observed: " << ex.what() << '\n';
26    }
27}

This is the direct answer when you are already using std::thread.

std::async and std::future Are Often Cleaner

If the work is really "run this task and give me a result later", std::async is usually simpler because exception propagation is built in. The future stores the exception and rethrows it on get().

cpp
1#include <future>
2#include <iostream>
3#include <stdexcept>
4
5int main() {
6    auto future = std::async(std::launch::async, [] {
7        throw std::runtime_error("task failed");
8        return 42;
9    });
10
11    try {
12        int value = future.get();
13        std::cout << value << '\n';
14    } catch (const std::exception& ex) {
15        std::cout << "caught in caller: " << ex.what() << '\n';
16    }
17}

That is often preferable to hand-rolling exception transport unless you specifically need raw thread control.

Multiple Threads Need a Policy

Once you have more than one worker, you need to choose an error policy:

  • stop on the first failure
  • collect all failures
  • convert failures into result objects

Here is a simple aggregation pattern:

cpp
1#include <exception>
2#include <iostream>
3#include <mutex>
4#include <thread>
5#include <vector>
6
7int main() {
8    std::vector<std::exception_ptr> errors;
9    std::mutex mutex;
10    std::vector<std::thread> workers;
11
12    for (int i = 0; i < 4; ++i) {
13        workers.emplace_back([i, &errors, &mutex] {
14            try {
15                if (i % 2 == 1) {
16                    throw std::runtime_error("failure in worker");
17                }
18            } catch (...) {
19                std::lock_guard<std::mutex> lock(mutex);
20                errors.push_back(std::current_exception());
21            }
22        });
23    }
24
25    for (auto& worker : workers) {
26        worker.join();
27    }
28
29    for (auto& error : errors) {
30        try {
31            std::rethrow_exception(error);
32        } catch (const std::exception& ex) {
33            std::cout << ex.what() << '\n';
34        }
35    }
36}

The synchronization matters because multiple workers may fail at the same time.

Design Around Tasks, Not Just Threads

A lot of code becomes simpler when you stop thinking "how do I catch a thread's exception?" and start thinking "how does this asynchronous task report success or failure?" Futures, promises, task executors, or message queues often express that design better than raw thread objects do.

The more structured the async abstraction, the less custom exception plumbing you usually need.

Common Pitfalls

  • Expecting a caller-side try block to catch an exception thrown on a worker thread.
  • Letting an exception escape a raw std::thread, which often leads to termination.
  • Checking shared exception state before joining the thread.
  • Writing to one shared exception_ptr from multiple workers without synchronization.
  • Using raw threads when a future-based task model would handle propagation automatically.

Summary

  • Exceptions do not cross thread boundaries automatically.
  • With raw threads, catch inside the worker and transport with std::exception_ptr.
  • With std::async, exceptions are propagated through the future.
  • Multiple workers require an explicit failure policy and synchronization.
  • If your real problem is task failure reporting, a higher-level async abstraction is often the better design.

Course illustration
Course illustration

All Rights Reserved.