Skip to content

Drain the async generator queue after it completes - #1838

Open
duo wants to merge 1 commit into
quickjs-ng:masterfrom
duo:fix/async-generator-drain-queue
Open

duo wants to merge 1 commit into
quickjs-ng:masterfrom
duo:fix/async-generator-drain-queue

Conversation

@duo

@duo duo commented Oct 21, 2026

Copy link
Copy Markdown

Once an async generator has completed, requests queued behind the head request can stay pending forever. This shows up when a consumer cancels a stream: a for await loop or a wrapper calls return() on an already finished generator while another next() or return() is still in flight, and the awaiting code never resumes.

Reproduction

// Each line is printed by Node.js and other engines; quickjs-ng prints neither.

// A request queued behind return() on a completed generator never settles.
const a = (async function* () {})();
a.next().then(() => {
    a.return(1);
    a.next().then(r => console.log("queued after return():", r.done));
});

// Requests queued while the body runs: only the first two settle.
let release;
const gate = new Promise(r => { release = r; });
const b = (async function* () { await gate; })();
b.next();
b.next();
b.next().then(r => console.log("third queued next():", r.done));
release();

Node.js prints:

third queued next(): true
queued after return(): true

qjs at master prints nothing and exits; the pending promises are simply never settled.

Cause

Two places stop processing the queue early:

  1. The fulfilled and rejected closures of AsyncGeneratorAwaitReturn must run AsyncGeneratorDrainQueue after settling the return() request. js_async_generator_resolve_function() (the magic >= 2 branch) settles the request and returns without resuming the queue.
  2. AsyncGeneratorDrainQueue keeps settling next() and throw() requests in the completed state until it reaches a return() or empties the queue. js_async_generator_resume_next() jumps to done after the first one. This also strands next() calls that were queued while the body was still running.

The fix calls js_async_generator_resume_next() after settling in the await-return callback, and continues the loop after next()/throw() in the completed state. A return() still stops the loop while its value is awaited.

Tests

tests/async-generator-drain-queue.js covers single and multiple requests queued behind return() (fulfilled and rejected), mixed next()/throw()/return() queues, and next() calls queued while the body awaits. It checks every result and the settlement order, which matches Node.js. It fails with promise is pending on master and with either half of the fix alone, and passes with both.

On macOS 15 (Apple clang 17):

  • make test: same result before and after, apart from the new test. The one failure, test_std.js "second pipe handler was not called", also fails on unmodified master in this environment.
  • make test262 (test262 5ef1e5723b): 40/82220 errors before and after, all listed in test262_errors.txt; no new or fixed tests.
  • The new test and make test under QJS_ENABLE_ASAN + QJS_ENABLE_UBSAN, and the new test under QJS_ENABLE_GC_STRESS: clean.

test262 checks return() on a completed generator (return-state-completed.js) but not with further requests queued behind it, which is why this was not caught there.

Once an async generator has completed, requests queued behind the head
request could stay pending forever:

    const it = (async function* () {})();
    await it.next();
    it.return(1);
    it.next(); // never settled

Two places stopped processing the queue early:

- The fulfilled and rejected closures of AsyncGeneratorAwaitReturn must
  run AsyncGeneratorDrainQueue after settling the return() request.
  js_async_generator_resolve_function() settled the request and returned
  without resuming the queue.

- AsyncGeneratorDrainQueue keeps settling next() and throw() requests in
  the completed state until it reaches a return() or empties the queue.
  js_async_generator_resume_next() left the loop after the first one,
  which also stranded next() calls that were queued while the body was
  still running.

This branch has not been deployed

No deployments
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