1 of 12

AsyncContext:

yield*

Nicolò Ribaudo (Igalia)

September 2025

2 of 12

Recap: AsyncContext and generators

Principle: other code you interact to cannot change your context.

2

function *fn() {

// context 1

yield 1;

// context 1

yield 2;

// context 1

}

let it = fn(); // context 1

// context 2

it.next();

// context 2

// context 3

it.next();

// context 3

3 of 12

Sometimes you want the opposite behavior

  • Getting an ambient AbortSignal from the caller of .next()
  • Running a callback in the context of .next() caller

And that's what you get with manually written iterators!

next() {

// context from the caller

}

3

// context 2

it.next();

// context 2

// context 3

it.next();

// context 3

4 of 12

Escape hatch? A wrapper

4

const fn = withNextContext(function* () {

// context 1

const [nextCallerSnapshot, nextValue] = yield 1;

// context 1

nextCallerSnapshot.run(() => {

// context 2

})

});

let it = fn(); // context 1

it.next();

// context 2

it.next();

// context 2

Doesn't work with class methods, no hoisting, no function name

5 of 12

Current yield behavior: pseudocode

5

function *fn() {

yield 1;

yield 2;

}

  1. Let yieldValue be 1.
  2. Let genContext be AsyncContextSnapshot().
  3. Pause with yieldValue, resume the caller.
  4. When resumed, let result be that value.
  5. AsyncContextSwap(genContext).
  6. Result is result.
  • Let yieldValue be 2.
  • Let genContext be AsyncContextSnapshot().
  • Pause with yieldValue, resume the caller.
  • When resumed, let result be that value.
  • AsyncContextSwap(genContext).
  • Result is result.

6 of 12

Current yield* behavior: pseudocode

6

function *fn() {

yield* innerIt;

}

  • Let iterable be innerIt.
  • Let genContext be AsyncContextSnapshot().
  • Let nextArg and res be undefined.
  • While not done,
    1. Set res to iterable.next(nextArg)
    2. Pause with res, resume the caller.
    3. When resumed, set nextArg to that value.
    4. AsyncContextSwap(genContext).
  • Result is res.

7 of 12

Idea: keep the context only for the generator own body

7

function *fn() {

yield* innerIt;

}

  • Let iterable be innerIt.
  • Let genContext be AsyncContextSnapshot().
  • Let nextArg and res be undefined.
  • While not done,
    • Set res to iterable.next(nextArg)
    • Pause with res, resume the caller.
    • When resumed, set nextArg to that value.
  • AsyncContextSwap(genContext).
  • Result is res.

let it = fn(); it.next();

it.next();

The context from here is propagated to innerIt.next()

8 of 12

How does this solve the problem?

8

9 of 12

Use case: reading a variable from the .next() caller

9

const ambientAbortSignal = ...;

function* longRunningTask() {

let val = yield 1;

const mergedSignal = ???;

}

I want to combine the generator's ambient abort signal with the one from the .next() caller. How do I do it?

10 of 12

Use case: reading a variable from the .next() caller

10

const ambientAbortSignal = ...;

function* longRunningTask() {

let [nextSignal, val] = yield* readingVariable(ambientAbortSignal, 1);

const mergedSignal = AbortSignal.any([ambientAbortSignal.get(), nextSignal]);

}

function readVariable(asyncVar, yieldValue) {

let done = false;

return { next(v) {

if (!done) {

done = true;

return { value: yieldValue, done: false };

}

return { value: [v, asyncVar.get()], done: true };

} };

}

11 of 12

Use case: run a callback in the previous .next() context

11

function* mapper(cb) {

let res;

while (true) {

res = cb(yield res);

}

}

???

12 of 12

Use case: running a callback in the .next() context

12

function* mapper(cb) {

let res;

while (true) {

res = yield* callInContext(res, value => cb(value));

}

}

function callInContext(yieldValue, callback) {

let done = false;

return { next(v) {

if (!done) {

done = true;

return { value: yieldValue, done: false };

}

return { value: callback(v), done: true };

} };

}