Skip to content

fix(generator): make try/catch real in the sync generator state machine (#4438 B2) - #4457

Merged
proggeramlug merged 2 commits into
mainfrom
worktree-issue-4438
Jun 4, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
worktree-issue-4438

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Addresses the B2 slice of #4438 (generator .throw()/.return() try/finally
edge cases): makes a generator's try/catch real in the state machine for
sync generators.

Root cause (deeper than the issue's framing)

The issue described B2 as "continuation-cloning edge cases in linearize.rs",
but probing showed the catch handler was entirely absent from the normal
.next() dispatch
— it existed only inside the .throw() closure. Two
consequences, both visible in test262:

  • (a) a throw executing inside a try during a plain .next() was never
    caught — it propagated straight out of next() (the [object] escaped
    failures);
  • (b) a yield inside a catch was swallowed — it was rewritten to an
    await in the throw closure (catch (e) { yield e } lost the yield).

Fix

  • linearize.rs — linearize the catch body into its own states (the guard
    now also fires when the yield lives only in the catch). The happy path skips
    the catch states; the catch's own states are excluded from the protected
    interval so a throw inside the catch escapes to an enclosing handler.
  • lower.rs — for sync generators, wrap the dispatch loop body in a real
    try/catch that routes a thrown error to the matching catch's
    catch_entry_state (or runs pending finally + completes the generator when
    unhandled). .throw() routes the same way and falls through to the
    continuation loop, so a yield inside the catch suspends correctly. The
    runtime-throw path uses an inclusive lower bound (>=) since a throw in a
    try's first state runs at exactly protected_start.

Async generators are untouched: the linearized catch states are dead code on the
async path (it still inlines the catch body in .throw()), so behavior is
byte-identical.

Results

test262 GeneratorPrototype/{throw,return}/try-*: 15 → 21 pass (all
try/catch cases). Broader sweep of every try-containing generator test:
0 regressions, 17 newly-fixed — the 6 GeneratorPrototype cases plus 11
language/expressions/yield yield*-delegation error-handling tests
(star-rhs-iter-*-err, from-catch, rhs-unresolvable).

Out of scope (remaining B2 — separate follow-up)

The 6 still-failing cases all involve a yielding finally (finally { yield N }), which needs a completion-record mechanism to thread abrupt completion
through the finally's states and re-raise afterward. Tracked as the remaining
B2-finally work under #4438.

…ne (#4438)

B2 slice of #4438. The catch handler previously existed only inside the
.throw() closure, not in the normal .next() dispatch, so (a) a throw
executing inside a try during a plain .next() escaped uncaught and (b) a
yield inside a catch was swallowed (rewritten to await).

Linearize the catch body into its own states; the happy path skips them.
A sync generator's dispatch loop is wrapped in a try/catch that routes a
thrown error to the matching catch's entry state (running pending finally
and completing the generator when unhandled). .throw() routes the same way
and falls through to the continuation loop so a yield inside the catch
suspends. The runtime-throw path uses an inclusive lower bound since a
throw in a try's first state runs at exactly protected_start.

Async generators are untouched (linearized catch states are dead on the
async path), so behavior is byte-identical.

test262 GeneratorPrototype/{throw,return}/try-*: 15 -> 21 pass. Broader
sweep of try-containing generator tests: 0 regressions, 17 newly-fixed
(6 GeneratorPrototype + 11 yield* delegation error-handling cases).
Remaining 6 failures all involve a yielding finally (separate follow-up).
@proggeramlug
proggeramlug merged commit 8551b78 into main Jun 4, 2026
13 checks passed
@proggeramlug
proggeramlug deleted the worktree-issue-4438 branch June 4, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant