How can naive resume() chaining overflow the stack?
This awaiter chains coroutines: each one resumes the next. A long chain of thousands of coroutines reliably overflows the stack.
struct ResumeNext {
std::coroutine_handle<> next;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {
next.resume();
}
void await_resume() {}
};
Identify the bug and explain the cause.
If await_suspend calls handle.resume() on the next coroutine, and that one resumes another, each resume() nests inside the previous stack frame. A long chain never unwinds and overflows the stack.
- ✗Calling
handle.resume()insideawait_suspendinstead of returning the handle - ✗Thinking heap-allocated frames mean coroutine chains can never overflow the stack
- ✗Believing the danger is only theoretical and ignoring deep producer/consumer chains
- →How does returning a
coroutine_handle<>fromawait_suspendbound the stack depth? - →Why is a tail call the key difference between safe and unsafe resumption?
The dangerous pattern
struct ResumeNext {
std::coroutine_handle<> next;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<>) {
next.resume(); // ❌ nested call — the stack frame is NOT popped
}
void await_resume() {}
};
When coroutine A does co_await ResumeNext{B}, B.resume() is called on top of A's stack frame. If B immediately does co_await ResumeNext{C}, another frame is added, and so on. A chain of thousands of coroutines is a guaranteed stack overflow.
The fix: symmetric transfer
struct ResumeNext {
std::coroutine_handle<> next;
bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<>) {
return next; // ✅ the compiler performs a tail call
}
void await_resume() {}
};
Returning the handle instead of calling resume() turns resumption into a tail call: the current frame is popped before transferring to next. Stack depth stays constant no matter how long the chain is.