Why is coroutine_handle::resume() potentially-throwing?

Raymond

In our explorations of making co_awaitable objects, we had largely been ignoring the possibility of the coroutine handle throwing an exception upon resume. But according to the language specification, the resume method (and its equivalent, the operator() overloaded function call operator) is potentially-throwing. Is this an oversight or an intentional decision?

Well, the noop_coroutine‘s coroutine handle does mark its resume() method as noexcept, so it’s not like the authors of the coroutine specification simply forgot about noexcept. They consciously put it on the resumption of a noop_coroutine, but omitted it from other coroutines.

What’s more, if you look at libraries that operate on coroutines, all of them treat the resume method as if it were noexcept.

What’s the deal?

Gor Nishanov explained it to me.

Allowing resume to throw was introduced in P0664R6 section 25, with this remark:

This resolution allows generator implementations to define unhandled_exception as follows:

  void unhandled_exception() { throw; } 

With this implementation, if a user of the generator pulls the next value, and during computation of the next value an exception will occur in the user authored body it will be propagate back to the user and the coroutine will be put into a final suspend state and ready to be destroyed when generator destructors is run.

Yeah but what does that all mean?

The scenario here is the use of coroutines as generators.

If a generator encounters an exception, the normal mechanism would be for the exception to be captured in the coroutine’s unhandled_exception method so that it can be re-thrown when the caller performs an await_resume. But if the generator is synchronous (performs no co_await operations), then it is more efficient to just let the exception propagate across the coroutine boundary directly to the caller.

The coroutine implementation (specifically, the promise) can indicate that it wants the exception to propagate by rethrowing the exception in unhandled_exception, rather than capturing it.

But if you’re not in the case of a synchronous generator (and when dealing with coroutines as tasks, you won’t be), then resume is indeed nonthrowing.

Bonus reading: Another reason for not marking resume() as noexcept is that resume() requires that the coroutine be suspended. The presence of a precondition means that, according to the Lakos Rule, the function should not be marked noexcept. This allows the implementation to choose to report the precondition violation in the form of an exception.

3 comments

Comments are closed. Login to edit/delete your existing comments

  • 紅樓鍮

    Funny then that Herb Sutter opposes the use of exceptions to report precondition violations (p0709r4). I side with Sutter on this matter.

    • Raymond ChenMicrosoft employee

      There are two different scenarios. One is production (precondition violation = death), and the other is unit testing (where you intentionally violate a precondition to see what happens). The idea is that you can have the exception inside a #ifdef _REPORT_PRECONDITION_VIOLATION_AS_EXCEPTION_FOR_TESTING block which is off in production but turned on by the unit test.

      • 紅樓鍮

        I don’t see value in testing code’s behavior when called with the wrong preconditions, because you won’t be calling them with the wrong preconditions anyway in useful code. For debug builds, I prefer firing assertions over throwing exceptions on precondition violations.

        This is different from actual runtime failures that do not result from incorrect programming (e. g. I/O errors), which do require defined behavior on failure (and thus should be tested for failure).