-
-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A slot called by qt, which is a coroutine: handling exceptions #97
Comments
Hi, generally, exceptions are pain in Qt, exactly for the reason you describe - how to handle an exception thrown from a slot invoked from an event loop? There's no (easy) way, really. Which is why exceptions in Qt are discouraged. With coroutines a new pattern emerges: error from asynchronous operations can propagate to caller the same way as result. My favorite pattern is using the proposed QCoro::Task<tl::expected<QByteArray, Error>> sendMessage(const QString &msg) {
if (!connected) {
co_return tl::make_unexpected(Error::NetworkError);
}
const QByteArray response = ....
co_return response;
}
// Depending on whether `onServerConnected()` would be a 'top-level" coroutine, you may or may not
// want to propagate an error further up the coroutine chain
QCoro::Task<> onServerConnected() {
const auto result = co_await sendMessage(...);
if (!result.has_value()) {
qWarning() << "Failed to send message, reason: " << result.error();
co_return;
}
const auto parsed_response = parseResponse(*result);
if (!parsed_response.has_value()) {
qWarnnig() << "Failed to parse message, reason: " << parsed_response.error();
co_return;
}
// do something with parsed_response
} |
I see. Well, it won't be possible for me to rework everything to std::expected (although I like the approach). I was wondering: would it be possible to assign a custom handler for unhandled exceptions? Or perhaps add an option to change the default behavior of silently ignoring the exceptions to "terminate the program"? That would help with debugging (I could log the exception) and possibly some exceptions coming out of logic errors (a |
I see two options here, if you want to stick to exceptions:
QCoro::Task<> onServerConnected() try {
...
...
} catch (const std::exception &e) {
qCritical() << "Unhandled exception in foobar:" << e.what();
std::terminate();
}
The idea is that if a coroutine which is invoked from an event loop throws an exception, QCoro will rethrow the exception immediately (which will propagate to Qt event loop and cause an abort), instead of storing it. On the other hand this would be somewhat violating the coroutine contract, which says that exception is rethrown inside the awaiter when the awaited coroutine throws, which implies that if there's no awaiter, the exception (as well as the coroutine result) are silently discarded. So I'm somewhat hesitant to do this, or at least it would have to be hidden behind some QCoro-wide global option you would call in |
Ideally, I would still like to preserve throwing from
Yes, I agree, it would have to be an option. I understand that it would imply some global state which is suboptimal. But it's something that's probably set only once in a program's lifetime, so it could be an environmental variable or maybe a compile-time option. Also, another possible solution: One more thought: in JS, if you |
This is the discussion thread for JS: nodejs/node#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. IMHO, non-awaited exceptions should just crash the program. Ignoring them could lead to undefined behavior and/or memory leaks. |
Coincidentally, this is possible with QCoro. We have to have a special trick where the coroutine is aware of the To put it shortly, yes, we are able to detect when coroutine has finished without being I like your idea of an additional template parameter to |
Some observation from my current implementation:
I might work around this by introducing an
Normally when a Qt slot throws and the exception propagates all the way into Qt's event loop, you only get a warning from Qt about an unhandled exception reaching the event loop, but the exception will propagate further up from the int main(int argc, char **argv) {
QCoreApplication app(argc, argv);
MyApp myApp;
try {
app.exec();
} catch (const std::exception &e) {
std::cerr << "The application has thrown an exception, the program will end now" << std::endl;
myApp.emergencyCleanup();
return 1;
}
return 0;
} This is not possible when handling the exception inside QCoro's coroutine code, the only way is to print some warning and then call |
The current PoC code lives in the |
Thanks for the update. I'm going to try the new feature.
Also, I was wondering whether it would be possible to implement the same thing One more thing: would it be possible to allow I don't really depend on the |
I have tried the new branch, but it seems that I'm doing something wrong. Exceptions are still being caught. Or rather, I would think that this program would crash, but it doesn't: #include <QCoreApplication>
#include <QTimer>
#include <QCoroTask>
QCoro::Task<void, QCoro::TaskOptions<QCoro::Options::AbortOnException>> my_coro()
{
throw std::exception();
co_return;
}
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
auto tmr = new QTimer();
QObject::connect(tmr, &QTimer::timeout, my_coro);
tmr->start(1000);
app.exec();
} |
Regarding the Generator & exceptions - the exception from the generator should be re-thrown when you dereference the iterator, which, combined with the Regarding Finally, regarding your test code: I pushed a fix to the branch, please retry :) |
I got this sample code: #include <iostream>
#include <QCoreApplication>
#include <QCoroGenerator>
QCoro::Generator<int> my_generator()
{
co_yield 123;
throw std::exception();
}
int main(int argc, char* argv[])
{
auto gen = my_generator();
auto it = gen.begin();
std::cerr << "*it = " << *it << "\n";
++it;
std::cerr << "*it = " << *it << "\n";
} It crashes with a segfault:
Also, I didn't realize I would have to derefence to iterator to rethrow the exception. I suppose it makes sense. I did think that the
My usecase was to just have a simple (synchronous) coroutine, that I could run and pause (yield). Without the coroutine I would need to have a function that would work kind of like a state machine, which is undesirable for me. But
Done: #100
Yep, now it seems to work. |
Also, it seems that it does not exactly where the iterator is, if one exception was thrown, you can advance the iterator as much as you want and the next dereference will throw the exception (it crashes with segfault right now, but that's a detail). This behavior kind of makes me think, that the #include <iostream>
#include <QCoreApplication>
#include <QCoroGenerator>
QCoro::Generator<int> my_generator()
{
co_yield 123;
throw std::exception();
co_yield 321;
}
int main(int argc, char* argv[])
{
auto gen = my_generator();
auto it = gen.begin();
++it;
++it;
++it;
++it;
++it;
*it;
} I should probably open another issue for this. |
I think I need to formalize the behavior of the iterator a bit more in the documentation (and check the code if it matches the expections describe below): When an exception is thrown from the generator, it terminates the generator and thus the next iterator becomes invalid. What you do in your example is undefined behavior: int main(int argc, char* argv[])
{
auto gen = my_generator();
auto it = gen.begin(); // *it == 123
++it; // *it == std::exception
++it; // it == gen.end()
++it; // undefined behavior: incrementing a past-the-end iterator
++it; // ditto
++it; // ditto
*it; // undefined behavior: dereferencing a past-the-end iterator
} I think exception handling in generators needs better documentation (and tests) :-) I'll try to look into it. Sorry for the delay in the terminating-generator, I wanted to complete another WIP change. I'll get back to the generators this week. |
I didn't realize, that throwing an exception would make the next iterator the .end iterator. I think that makes sense: if an error occured like that, you shouldn't be able to generate more stuff. Thanks for looking into this. I think this can be solved in the documentation (as you suggest). |
Hi, regarding the exceptions in generators:
Everything is done in #105. |
Hi, I was wondering: what do you think about merging feature/task-exception-handling into main? |
Hi, after more thought, I'm not really comfortable with the additional template argument, so I'm not going to merge the branch in the current state. I'm trying to think of some better way to implement thids, possibly being able to auto-detect whether we are being called from inside the event loop. |
Hi, is there any progress on this? I can see that the development is QCoro is continuing, so maybe there's been a patch that addresses this? For now, I've been using my own branch here, but I'm not able to rebase it anymore, because QCoro changed too much for me to easily do that. Do you think there's a chance for QCoro to rethrow unawaited (and therefore uncatchable) exceptions instead of silently throwing them away? I can't really think of a usecase, where I would be happy about just ignoring all exceptions (other than wrapping my whole program in a try-catch block). I suppose it's really a matter of opinion. However, I'm not really happy about maintaining my own branch. I understand, that this issue is quite difficult to solve and it might take a while. If that's the case, maybe I'll just revert back to catching all exceptions with try-catch in the "top-level" coroutine and calling |
Hi, sorry there's been no progress on this front. I couldn't (yet) find a good way how to rethrow the unhandled exception from a non-coawaited coroutine. I understand it would be consistent with regular functions if an uncaught exception would have been logged inside QCoreApplication and propagated to |
Hi, I have this kind of code:
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 thatdoStuff
itself must be a couroutine. My issue is that ifdoStuff
throws, I have no way to handle that exception, because Qt doesn't call it withco_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?
The text was updated successfully, but these errors were encountered: