In a recent article, I talked about exceptions as an effective tool to handle unexpected conditions in C++ applications.

As multithreading is a pretty complicated topic already, I decided to leave it out from that discussion. In this article, having described the theory behind a correct C++ exception usage, I’ll further investigate them in relation to multithreading.

I will address two problems in this article:

  • What happens when an exception thrown by a thread is uncaught, and the rationale behind it.
  • How to propagate exceptions to handlers outside the thread that created them.

Silence is a form of communication

An uncaught exception (an exception that is thrown and that doesn’t encounter any specific handler in the call stack) terminates the program. This is a decision in line with C++design philosophy: to provide the most generic behaviour possible as the default, and give the users the ability to build a different behaviour on top of that.

When a program doesn’t include a generic catch clause in its topmost logic, typically in main, that must be interpreted as an explicit declaration by its developers:

“If this program encounters exceptional situations we didn’t think about, we do want it to be terminated immediately.”

This is a very important statement to make, and it’s the default position a developer should take.

A program can fail by either failing to produce any result at all, or by producing incorrect results.

In the vast majority of cases, the second form of failure is the one with greater impact. For example, imagine a healthcare management program. If undefined behaviour leads to its termination, this will require human intervention and following manual procedure to produce a minimal level of assistance; but if it silently causes incorrect output, it can easily mix up medications and kill people.

Threads live in programs

When an uncaught exception reaches the topmost logic level of a thread, this means the integrity of the thread data cannot be guaranteed. As threads within a program share data arbitrarily, it is not possible to guarantee that the rest of the program is not in an undefined state.

For this reason, the default action in that case is to terminate the whole program. Unless the developers have taken specific measures to ensure that the program is not in an undefined state, C++ assumes the thread and the whole program where it runs are now in an undefined state, and should be terminated as soon as possible.

Handling exceptions in a multithreaded program

By now, it should be clear why it is a requirement for the developers to explicitly handle exceptions also in the topmost logic of every thread, if the default behaviour of terminating the whole host program is not what they need.

But multithreaded programs offer an additional challenge: the exception handler capable of managing errors encountered by a certain thread may be running in a different one.

The C++11 and following standards introduce several tools that can be employed to address this problem. We’ll see some of them, in increasing order of complexity and flexibility.

Parallelism through async()

The standard library function std::async provides a simple and relatively powerful device to run code asynchronously, and potentially in a parallel thread. Dwelling in how std::async decides to use parallelism to optimise concurrent execution is beyond the scope of this article; what I want to talk about here is how this function guarantees that any exception (derived from std::exception) gets propagated to the thread expecting a result.

Invoking std::async returns a std::future, an object that will either yield a result or throw an exception after the code run by async has completed its execution. If an exception is thrown by that code, it will be captured and repeated in the thread waiting for a result.

#include <iostream>
#include <vector>
#include <future>

int faulty(std::size_t element)
{
    static std::vector<int> shortOne{1,2,3,4,5};
    return shortOne.at(element); // This will throw.
}

int main() {
    try {
        auto future = std::async(std::launch::async, &faulty, 100);
        std::cout << "Element at position 100: " << future.get() << '\n';
        return 0;
    }
    catch(std::exception& error) {
        std::cout << "Huston, we have a problem: " << error.what() << '\n';
        return 1;
    }
}

On line 13, faulty is invoked asynchronously (typically in a separate thread). On line 14, future.get() should yield the value returned on line 8; but in this case, the function terminates with an exception, which will be thrown here and finally caught by our handler on line 17.

Decorated tasks via packaged_task

The abstraction offered by std::async is useful for simple parallel tasks, but at times it’s necessary to have more control on the execution of the target thread.

The class std::thread encapsulates the system threads with minimal overhead. It spawns a system thread and start executing the function it receives in the constructor as soon as possible, which is then put in charge of handling exceptions, and eventually propagate them as necessary. The standard library provides a lightweight machinery that automate this, and other tasks that are commonly needed by C++ programs using std::thread.

The class std::packaged_task wraps an arbitrary function to be run in a std::thread, providing the user with the same machinery asstd::async. Most notably, it can be used to create a future to access the result of the concurrent execution, or to propagate a std::exception that may be thrown in the process.

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

void countTo(int limit, int failAt) {
    using namespace std::chrono_literals;
    for(int i = 1; i <= limit; ++i) {
        std::cout << "Counting " << i << '\n';
        if(i == failAt) {
            throw std::runtime_error("I was doomed to fail");
        }
        std::this_thread::sleep_for(250ms);
    }
}

int main() {
    try {
        std::packaged_task<void(int, int)> parallel_countTo{countTo};
        auto result = parallel_countTo.get_future();
        auto thread = std::thread{std::move(parallel_countTo), 10, 5};
        thread.join();
        result.get();

        std::cout << "countTo was completed\n";
        return 0;
    }
    catch(const std::exception& error) {
        std::cout << "The thread died unexpectedly with error: " << error.what() << '\n';
        return 1;
    }
}

On line 20, countTo is encapsulated in a packaged task, and on line 21 we get hold of the future that will be used to query the result or propagate the error.

Notice that the code needs both to join the thread (on line 23) and get the result (24). At that point, the exception generated on line 12 is propagated, and received by the handler on line 29.

Custom error handling in thread main function

std::async and std::packaged_task are good general purpose utilities, but sometimes it’s necessary to manually control the error handling performed by the thread itself.

For example, as described in the previous article, it may be useful to have an application-specific exception class hierarchy, separated from std::exception; or it may be necessary to perform some pre-processing of the exception at thread level, before it is propagated to other interested threads.

The following example implements a thread manually performing error handling, and using a std::future to communicate results and propagate errors.

#include <atomic>
#include <functional>
#include <future>
#include <iostream>
#include <queue>
#include <thread>
#include <variant>

template<class Result>
struct OpQueue {
    OpQueue(): thread_{&OpQueue::run, this} {}
    ~OpQueue() {
        push(Termination{});
        thread_.join();
    }

    std::future<Result> queueOp(std::function<Result()> op) {
        std::promise<Result> promise;
        auto future = promise.get_future();
        push(std::make_pair(std::move(promise), std::move(op)));
        return future;
    }

private:
    using Operation = std::pair<std::promise<Result>, std::function<Result()>>;
    using Termination = bool;
    using OptionalOp = std::variant<Operation, Termination>;
    std::queue<OptionalOp> thingsToDo_;
    std::mutex mtxThingsToDo_;
    std::condition_variable cvNewThing_;
    std::thread thread_;

    void push(OptionalOp&& op) {
        std::lock_guard<std::mutex> guard{mtxThingsToDo_};
        thingsToDo_.push(std::move(op));
        cvNewThing_.notify_one();
    }

    // We can grant we're not raising an exception.
    void run() noexcept {
        while(true) {
            OptionalOp toDo;

            try {
                // Lock scope will not include function execution.
                std::unique_lock<std::mutex> guard{mtxThingsToDo_};
                cvNewThing_.wait(guard, [this]{return !thingsToDo_.empty();});
                toDo = std::move(thingsToDo_.front());
                // Is this a termination request?
                if(std::holds_alternative<Termination>(toDo)) {
                    return;
                }
                thingsToDo_.pop();
            }
            catch(const std::exception& error) {
                // This would be a system error on conditions, mutex or memory.
                std::cout << "We got a fatal error in OpQueue internals: " <<
                    error.what() << '\n';
                // A real world application may prefer to abort().
                continue;
            }

            try{
                auto& [promise, op] = std::get<Operation>(toDo);
                promise.set_value(op());
            }
            catch(...) {
                // Any bad outcome will be notified cleanly.
                std::get<Operation>(toDo).first.set_exception(std::current_exception());
            }
        }
    }

};

int main()
{
    try {
        OpQueue<int> ops;
        std::vector<std::function<int()>> thingsToDo = {
            []{return 1 + 1;},
            []{return 2 + 2;},
            []{return std::vector<int>{1, 2, 3}.at(5);}, // This will throw.
            []{return 4 + 4;}
            };
        std::queue<std::future<int>> results;

        for(auto& toDo: thingsToDo) {
            results.emplace(std::move(ops.queueOp(toDo)));
        }

        int count = 0;
        while(!results.empty()) {
            auto value = results.front().get();
            results.pop();
            std::cout << "Result n. " << ++count << " is " << value << '\n';
        }
    }
    catch(const std::exception& error) {
        std::cout << "Huston, we have a problem: " << error.what() << '\n';
    }

    return 0;
}

The OpQueue class receives a list of arbitrary functions to be run in its own thread, started on line 11, which takes the run() method as its main function on line 40.

When OpQueue accepts a new task on line 17, and then returns a future that will be used to store a result or propagate the exception through the usual future::get.

On line 55, a custom error handling takes care of problems the O/S may encounter while using system level resources as mutexes and condition variables.

On line 60, the exceptions that may be thrown by the queued operations are caught and repeated to the associated future, but the thread continues processing other operations.

The task declared on line 83 will actually raise an exception, which will be fired on line 94. We may chose to catch it and continue, but the program terminates with the catch clause on line 99.

Application specific error forwarding

Writing custom catch-all handlers at the topmost thread logic for every thread is a viable strategy for small programs, or for applications that start a limited number of threads.

In applications that could start threads arbitrarily, it’s preferable to provide some sort of standardized top level error handling.

The following example provides guarantees similar to those offered by packaged_task, but shows how to implement them by encapsulating std::thread into a richer class.

#include <chrono>
#include <functional>
#include <iostream>
#include <thread>

class Application {
public:
    // Usual machinery omitted for brevity.

    void log_error(const std::string& what) {
        std::cerr << "LOG ERROR: " << what << '\n';
    }

    class thread
    {
    public:
        thread(const thread&) = delete;

        thread(thread&& other) noexcept:
            name_{std::move(other.name_)}, runner_{std::move(other.runner_)}
            {}

        void join() {
            runner_->join();
        }

        // Similarly detach()
    private:
        template<class Function, class... Args>
        explicit thread(Application* owner, const std::string& name, Function&& f, Args&&... args):
            name_{name} {
            runner_ = std::make_unique<std::thread>([&f, &args..., owner, this] {
                try {
                    f(args...);
                }
                catch(const std::exception& error){
                    owner->log_error(
                        std::string{"Thread `"} + name_ + "` terminated with error: " + error.what());
                }});
            }

        std::string name_;
        std::unique_ptr<std::thread> runner_;
        friend class Application;
    };

    template<class Function, class... Args>
    thread make_thread(const std::string& name, Function&& f, Args&&... args) {
        return thread{this, name, std::forward<Function>(f), std::forward<Args>(args)...};
    }
};

void count_to(int limit, int fail_at) {
    using namespace std::chrono_literals;
    for(int i = 1; i <= limit; ++i) {
        std::cout << "Counting " << i << '\n';
        if(i == fail_at) {
            throw std::runtime_error("I was doomed to fail");
        }
        std::this_thread::sleep_for(250ms);
    }
}

int main() {
    Application the_app;
    auto good_thread = the_app.make_thread("Good Thread", count_to, 10, 0);
    auto bad_thread = the_app.make_thread("Bad Thread", count_to, 10, 5);
    good_thread.join();
    bad_thread.join();
    // Note: we should catch exceptions here too.
    return 0;
}

Sophisticated applications often need a class representing themselves, and offering access to an application framework to take care of the ecosystem used by the rest of the program; for example, logging infrastructure, dynamic configuration, authentication services ecc.

In this example, I wrote a class named thread, meant to be used as the application framework specific thread. Here, it just provides a decoration (the parameter name) and the ability to access the application itself through a naked pointer.

On line 36, we provide a framework specific thread error handler. In the example, we just log the exception; a similarly valid solution would be that of implement a Application::handle_exception(const std::exception&) method, with the obvious meaning of having a standardized fatal error management at application level.

Also, notice that the code on lines 66 and 67 clearly shows how the application is logically responsible for the threads it creates.


In short, multithreading presents an additional challenge when it comes to C++ exceptions (besides the other many challenges they already present), but both the language and the standard library provide various tools to deal with the problem at different levels of flexibility and ease of use.

Writing a bare std::thread (or a system thread started in a C++ program by other means) not handling generic C++ exceptions is analogous to declare: “the developers have estimated that, in case an unknown condition is met, it is preferable to terminate the program immediately rather than being unable to guarantee the correctness of its result.”