Skip to content

break label out of a switch in an async generator throws TypeError reading 'done' #9199

Description

@proggeramlug

A break label targeting a labeled for…of from inside a switch, in an async generator, throws instead of breaking.

async function* gen(xs: number[]) {
  loop: for (const x of xs) {
    switch (x) { case 1: yield "y1"; break; case 2: break loop; default: yield "d" + x; }
  }
}
(async () => { const acc: string[] = []; for await (const v of gen([1,3,2,4])) acc.push(v); console.log(acc.join(",")); })();
result
node 26.5.1 y1,d3
perry Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'done')

The done read failing points at the async-iterator protocol: the labeled break appears to leave the generator state machine without producing a well-formed iterator result, so for await reads .done off undefined. The plain break in case 1 is fine — it is the labeled one that targets the enclosing for…of that fails.

Confirmed pre-existing on main by reverting crates/perry-transform/src/generator/linearize.rs to main and rebuilding: identical error, exit 1 both ways. Found while auditing #9189; that PR fixes the labeled-switch case (#9186) and this async-generator shape is not in its scope.

Sibling: the async function form with a labeled for…of hangs rather than throwing, filed separately.

Activity

  1. proggeramlug commented on Sep 4, 2026

    @proggeramlug
    ContributorAuthor

    Taking this. I'll reproduce against current main (which already carries #9189), work out why the labeled break leaves the async-generator state machine without a well-formed iterator result, add a gap regression, and open a PR.

  2. proggeramlug commented on Sep 4, 2026

    @proggeramlug
    ContributorAuthor

    Re-measured against current main (28c292517, which carries #9189). Two things changed since this was filed, and the second is why the issue should stay open.

    This issue's own repro now passes. The one-level shape — break loop from a switch inside a single labeled for…of — prints y1,d3, matching node. #9189 covered it after all, despite the note here that it was out of scope.

    The defect it describes is still live, one level deeper, with the identical error. The switch turns out to be a red herring; the trigger is a labeled break/continue that targets an outer loop from inside a nested loop, after a yield. Three isolated programs, each on its own:

    // p6 — break an outer label from the inner loop
    async function* p6() { O: for (const x of [1,2]) { I: for (const y of [0,1]) { yield "p6"+x+y; break O; } } }
    // node: p610      perry: TypeError: Cannot read properties of undefined (reading 'done')
    
    // p5 — continue an outer label from the inner loop
    async function* p5() { O: for (const x of [1,2]) { I: for (const y of [0,1]) { yield "p5"+x+y; continue O; } } }
    // node: p510,p520     perry: NO OUTPUT AT ALL — the `for await` never yields and never settles
    
    // pD — this issue's shape with the switch deleted
    async function* d() { outer: for (const x of [1,2]) { inner: for (const y of [0,1]) {
      if (x+y === 3) break outer; if (x+y === 1) { yield "D"+x+y; continue inner; } } } }
    // node: D10      perry: TypeError: Cannot read properties of undefined (reading 'done')

    p6's error is character-for-character the one reported here, so this is the same defect, not a new one.

    What still works, which brackets it tightly:

    shape result
    labeled continue on the same loop, after a yield OK
    labeled continue of the inner label from the inner loop OK
    unlabeled continue after a yield OK
    labeled continue over a while OK
    labeled break/continue in a sync generator, same shape OK
    break outer from a switch in the inner loop, when nothing yields in the inner loop OK
    break/continue of an OUTER label from a nested loop, after a yield broken

    So the surviving hole is specifically the cross-loop target: one linearized loop's label resolved from inside another linearized loop, where the inner loop has a suspension point. continue and break fail differently (silent drop vs. malformed iterator result), which suggests the target resumption state is wrong rather than absent.

    Also worth noting for whoever picks this up: the sibling async function hang mentioned at the bottom of this issue does not reproduce — asyncfn-break-label returns f1,f3, matching node.

    I'm working on this now; probes are on the build host under /root/cc9575/ts/iso_p5.ts, iso_p6.ts, iso_pD.ts.

  3. added a commit that references this issue on Sep 4, 2026
    70e411d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions