You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
async generators do not capture async context #42237
i dunno... its technically an abstraction that's outside of promises, but in terms of the usage of apis like AsyncLocalStorage, i think i would expect hooks to apply to them.
next() is a sync call which runs using the asyncId of the caller. Doesn't seem that wrong to me.
e.g. if you use s.run('hello', () => g.next()) the context is visible in this next() call. in your sample code next is called directly after returning from s.run() therefore the store not active anymore.
If the generator would capture the id where it was created everything after await next() would end up in this context. Hard to tell if this is good or bad.
But you also have the option to not capture at creation time, but rather, at a later point.
However, if generators captured context, you would NOT have any way to pass ANY different context via .next. No matter which context you used, it would replace it with the captured one.
Use case
What made me understand this wasn't a bug, was working on a library for structured concurrency. As part of the library, I pass a task-local AbortSignal which allows essentially any async function to be abortable, with no extra code, simply by calling/awaiting functions that are abortable via the task-local. Any functions in the middle needn't even be aware that they are abortable. The signal is passed forward via task-local without needing an explicit parameter, and an abort causes an exception which can move back up the stack through any number of functions so long as they don't try to catch it.
When it came to handling generators, I suddenly had to face the question: If one task creates a generator, and then a second task iterates over the generator, and then the second task aborts (via its task-local AbortSignal), what should happen in the generator?
Well, if the generator does not abort, then the second task would become stuck awaiting the generator's .next. which could very well be in the middle of a long, or even a frozen action. For example, if the generator is waiting for network traffic, and the second task handles the network timeout by aborting on a timer. If the abort does not propagate into the generator, the network operation performed by the generator would not abort, causing both the generator, and the second task, to remain stuck.
If async generators did capture the context at creation time, passing the AbortSignal from the iterating task into the generator would require a complex and ugly workaround, if it was at all possible. It might not even be doable as a wrapper, and may require the generator itself to include code to receive and use a passed value for the task-local context.
This is why I think the functionality should remain as-is. Those who need a captured context have an easy enough workaround (which could easily be made into a standard utility function similar to util.promisify). At most, this is a documentation issue.
Activity
benjamingr commentedon Mar 7, 2022
@devsnek is this v8 not firing the right promise hooks or on our side (or you haven't had a chance to investigate yet)?
devsnek commentedon Mar 7, 2022
I'm not sure.
Flarna commentedon Mar 7, 2022
Should async generators capture the async context?
devsnek commentedon Mar 7, 2022
i dunno... its technically an abstraction that's outside of promises, but in terms of the usage of apis like AsyncLocalStorage, i think i would expect hooks to apply to them.
Flarna commentedon Mar 7, 2022
next()is a sync call which runs using the asyncId of the caller. Doesn't seem that wrong to me.e.g. if you use
s.run('hello', () => g.next())the context is visible in thisnext()call. in your sample codenextis called directly after returning froms.run()therefore the store not active anymore.If the generator would capture the id where it was created everything after
await next()would end up in this context. Hard to tell if this is good or bad.iamgabrielsoft commentedon Mar 25, 2022
Are you missing the yield keyword, I can't find it
Your using a generator
benjamingr commentedon Mar 25, 2022
User expectation would probably be "yes"
RMHonor commentedon Jun 15, 2023
Using inspiration from open-telemetry/opentelemetry-js#2951 (comment), I was able to build a generic wrapper to work around this issue in the mean time.
anonrig commentedon Feb 21, 2024
cc @nodejs/tsc who would have the most information to resolve this bug?
mcollina commentedon Mar 20, 2024
I think @bengl can coordinate this.
Flarna commentedon Mar 21, 2024
FWIW there is an ongoing discussion in scope of TC39 AsyncContext proposal regarding the same topic.
mrmar99 commentedon Apr 2, 2024
Got this problem when I was writing logging middleware for nice-grpc server.
Middleware:
In
withContextfunction I useenterWithto attachstoreDatato mystore:Now, I have access to data in storage in my
callbackfunction by importingstoreand callingstore.getStore().Let me know if there is a better way.
SlugFiller commentedon Jul 24, 2025
After being "burned" by this, I delved more into it, and realized that this is not a bug.
If you want to capture the context for in generator, you can use a proxy like this:
You can turn your any regular generator into a capturing generator like this:
But you also have the option to not capture at creation time, but rather, at a later point.
However, if generators captured context, you would NOT have any way to pass ANY different context via
.next. No matter which context you used, it would replace it with the captured one.Use case
What made me understand this wasn't a bug, was working on a library for structured concurrency. As part of the library, I pass a task-local
AbortSignalwhich allows essentially anyasyncfunction to be abortable, with no extra code, simply by calling/awaiting functions that are abortable via the task-local. Any functions in the middle needn't even be aware that they are abortable. The signal is passed forward via task-local without needing an explicit parameter, and an abort causes an exception which can move back up the stack through any number of functions so long as they don't try to catch it.When it came to handling generators, I suddenly had to face the question: If one task creates a generator, and then a second task iterates over the generator, and then the second task aborts (via its task-local
AbortSignal), what should happen in the generator?Well, if the generator does not abort, then the second task would become stuck awaiting the generator's
.next. which could very well be in the middle of a long, or even a frozen action. For example, if the generator is waiting for network traffic, and the second task handles the network timeout by aborting on a timer. If the abort does not propagate into the generator, the network operation performed by the generator would not abort, causing both the generator, and the second task, to remain stuck.If async generators did capture the context at creation time, passing the
AbortSignalfrom the iterating task into the generator would require a complex and ugly workaround, if it was at all possible. It might not even be doable as a wrapper, and may require the generator itself to include code to receive and use a passed value for the task-local context.This is why I think the functionality should remain as-is. Those who need a captured context have an easy enough workaround (which could easily be made into a standard utility function similar to
util.promisify). At most, this is a documentation issue.mcollina commentedon Jul 29, 2025
Thanks for the good write up, I'm going to close this.