Summary
An await inside a finally block of a real async generator (async function*) is lowered to a raw Expr::Await inside its linearized finally state instead of an async suspend, so codegen (fs_await.rs, !ctx.is_async_fn) emits the blocking busy-wait (js_wait_for_event). This is the same class of deadlock as #8681 (await-in-catch), but in a different path: the #4438 B2-finally lowering in crates/perry-transform/src/generator/lower/abrupt.rs, not the was_plain_async catch path fixed by #8707.
Scope
- The
was_plain_async closure/function path (build_async_catch_route_body_direct) is not affected — the closure test async_closure_rewrite_leaves_no_residual_await covers in-finally and passes.
- Only the real async-generator finally path leaves the residual. Minimal repro (transform-level):
async function* g() {
try { yield 0; }
finally { await Promise.resolve(); } // <- becomes raw Expr::Await in a finally state
}
After transform_async_to_generator + transform_generators, the finally state contains LocalSet(__sent, Await(..)) (2 residual awaits — normal + abrupt completion copies) rather than an async suspend.
Likely fix direction
Analogous to #8707's catch fix: route the finally through its linearized FinallyRoute.finally_entry_state and stop materializing the finally body's suspends as raw awaits (the finally-state emission / build_finally_run_stmts interaction for the async path). This touches the heavily-patched #4438/#4374 abrupt-completion routing, so it wants its own change + behavioral validation (a try { await } finally { await } battery vs Node), separate from the catch fix.
Detection
The regression guard already exists: async_generator_linearizes_every_await_position in async_to_generator.rs had an await-in-finally case (removed from that test in #8707 and pointed here). Re-adding it is the acceptance test for this issue.
Found while landing #8681/#8707 (natively-compiled Claude Code -p streaming deadlock).
Summary
An
awaitinside afinallyblock of a real async generator (async function*) is lowered to a rawExpr::Awaitinside its linearized finally state instead of an async suspend, so codegen (fs_await.rs,!ctx.is_async_fn) emits the blocking busy-wait (js_wait_for_event). This is the same class of deadlock as #8681 (await-in-catch), but in a different path: the#4438B2-finally lowering incrates/perry-transform/src/generator/lower/abrupt.rs, not thewas_plain_asynccatch path fixed by #8707.Scope
was_plain_asyncclosure/function path (build_async_catch_route_body_direct) is not affected — the closure testasync_closure_rewrite_leaves_no_residual_awaitcoversin-finallyand passes.After
transform_async_to_generator+transform_generators, the finally state containsLocalSet(__sent, Await(..))(2 residual awaits — normal + abrupt completion copies) rather than an async suspend.Likely fix direction
Analogous to #8707's catch fix: route the finally through its linearized
FinallyRoute.finally_entry_stateand stop materializing the finally body's suspends as raw awaits (the finally-state emission /build_finally_run_stmtsinteraction for the async path). This touches the heavily-patched#4438/#4374abrupt-completion routing, so it wants its own change + behavioral validation (atry { await } finally { await }battery vs Node), separate from the catch fix.Detection
The regression guard already exists:
async_generator_linearizes_every_await_positioninasync_to_generator.rshad anawait-in-finallycase (removed from that test in #8707 and pointed here). Re-adding it is the acceptance test for this issue.Found while landing #8681/#8707 (natively-compiled Claude Code
-pstreaming deadlock).