qcoro: A slot called by qt, which is a coroutine: handling exceptions

Hi, I have this kind of code:

#include <iostream>
#include <QTimer>
#include <QCoreApplication>
#include <QCoroSignal>

struct Response {

};

class ServerConnection : public QObject {
    Q_OBJECT
public:
    ServerConnection()
    {
        // connect to the server...
        emit serverConnected();
    }
    Q_SIGNAL void serverConnected();

};

QCoro::Task<Response> sendMessage();

QCoro::Task<> onServerConnected()
{
    std::cerr << "doStuff()\n";
    // ... do stuff, maybe use sendMessage with co_await
    //
    // ...
    //
    throw std::runtime_error("Can't catch this");
}

int main(int argc, char* argv[])
{
    QCoreApplication app(argc, argv);
    auto srv_conn = new ServerConnection();
    QObject::connect(srv_conn, &ServerConnection::serverConnected, onServerConnected);
    app.exec();
}

I want to rework my current app to use qcoro. However, I don’t want to rewrite all of the stuff at once. In the above code, I’d like to use coroutines inside the doStuff function. This means that doStuff itself must be a couroutine. My issue is that if doStuff throws, I have no way to handle that exception, because Qt doesn’t call it with co_await (I’m not even sure how that would work!).

What do you think is the correct approach? Should I just wrap my “top-level” couroutine with a try-catch block and handle the exceptions there?

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Comments: 20 (10 by maintainers)

Most upvoted comments

Ideally, I would still like to preserve throwing from co_await coro;, and only abort, when the coroutine is NOT awaited. I’m not sure if that’s even possible.

Coincidentally, this is possible with QCoro. We have to have a special trick where the coroutine is aware of the Task’s lifetime. Normally the lifetime of a coroutine is bound to the lifetime of the Task, so when Task is destroyed, the coroutine is destroyed. But this only works when the coroutine is always co_awaited, which is not the case with a coroutine being called from an event loop - the event loop will discard the Task returned from the coroutine, which would normally destroy the coroutine before it would start. So what we do is that if Task is destroyed while the coroutine is still running, it will not destroy the coroutine, but instead the coroutine will self-destruct when it reaches the final suspend point.

To put it shortly, yes, we are able to detect when coroutine has finished without being co_awaited, so we could rethrow the exception at that point, if it holds any.

I like your idea of an additional template parameter to Task to enable this behavior. I’ll try to put something together and send you a link to PR so you can try it out.

This is the discussion thread for JS: https://github.com/nodejs/node/issues/20392 It seems that it’s a matter of opinion rather than some kind of a technical issue. It seems that the creators of Node.js eventually agreed that unhandled rejections should just terminate the program.

If similar behavior were to be implemented in QCoro, the default would be to terminate on unhandled exceptions. co_await can still rethrow them as normal.

IMHO, non-awaited exceptions should just crash the program. Ignoring them could lead to undefined behavior and/or memory leaks.