Repository navigation
The store when using enterWith/exitWith in asyncLocalStorage is not "stacked" #36683
Description
Activity
- addedasync_hooksIssues and PRs related to the async hooks subsystem.Issues and PRs related to the async hooks subsystem.
on Dec 30, 2020 @nodejs/async_hooks
@giltayar
asyncLocalStorage.exit()is not supposed to work in a "stacked" way. As its doc says:This methods runs a function synchronously outside of a context and return its return value. The store is not accessible within the callback function or the asynchronous operations created within the callback.What you faced in your snippet is a consequence of the described behavior, i.e. the context will always be
undefinedwithinals.exit()'s callback. If you need to return to the previous context (i.e. store), you need to do theals.getStore()call within the previous async call in the chain, just like you did it here:asyncLocalStorage.run({depth: 1}, () => { assert.deepStrictEqual(asyncLocalStorage.getStore(), {depth: 1}) asyncLocalStorage.run({depth: 2}, () => { // new scope (`als.run()` acts as if it would be a new async call in the chain) assert.deepStrictEqual(asyncLocalStorage.getStore(), {depth: 2}) }) // here we're in the scope of the previous async call assert.deepStrictEqual(asyncLocalStorage.getStore(), {depth: 1}) })
Also,
als.enterWith()simply replaces the context with the provided object/primitive, so it doesn't have the behavior you expect. In general, this method should be used with precaution as it doesn't limit store's lifetime/scope.@puzpuzpuz I thought this is maybe the defined behavior, but it wasn't clear from the documentation. If this is the defined behavior, then, yes, this issue can be closed. I'll wait a bit to see if anybody else has any input, and close it in a day or two if not. Thanks for the clarification!
Reacted by Andrei Pechkurov and Benjamin GruenbaumI thought this is maybe the defined behavior, but it wasn't clear from the documentation. If this is the defined behavior, then, yes, this issue can be closed.
Looks like the doc are a bit misleading. I've created #36705 as an attempt to make things more clear.
Reacted by Benjamin Gruenbaum- added a commit that references this issue
on Jan 2, 2021 - added a commit that references this issue
on Jan 12, 2021 Yeah, ALS is not in any way stacked. The current storage value is stored directly on the current resource, so it's a one-to-one relationship--any history would be lost.
Reacted by Benjamin GruenbaumYeah, ALS is not in any way stacked. The current storage value is stored directly on the current resource, so it's a one-to-one relationship--any history would be lost.
This comment threw me off a little when I first read it, so expanded the OP's nested
enterWith()example to include branching and asynchronous operations as well, hoping that that will help me and others to understand better what's going on behind the scenes:var { AsyncLocalStorage } = require("async_hooks"); var assert = require("assert"); const asyncLocalStorage = new AsyncLocalStorage(); asyncLocalStorage.run({ depth: 1 }, () => { assert.deepStrictEqual(asyncLocalStorage.getStore(), { depth: 1 }); asyncLocalStorage.run({ depth: 2 }, () => { setTimeout(() => { assert.deepStrictEqual(asyncLocalStorage.getStore(), { depth: 2 }); }, 2000); }); asyncLocalStorage.run({ depth: 3 }, () => { setTimeout(() => { assert.deepStrictEqual(asyncLocalStorage.getStore(), { depth: 3 }); }, 1000); }); assert.deepStrictEqual(asyncLocalStorage.getStore(), { depth: 1 }); });
What does this tell us? Even though you cannot directly access the values that were previously bound to the specific
AsyncLocalStorageinstance, all the values are stored somewhere, in memory, and they indeed form a stack (one or multiple stacks -- in the example both{ depth: 3 }and{depth: 2}have{ depth: 1 }as parent.They only form a stack in the sense that the code itself forms a stack. When you do
asyncLocalStorage.run({ depth: 2 }, ...)it is internally creating a newAsyncResourcewhich it enters for the lifetime of the callback and all async activity within that callback will be connected to thatAsyncResource. When you reach the check at the end fordepth: 1, that passes because theAsyncResourcestack internal to the otherasyncLocalStorage.run(...)calls has exited by that point, returning to the originalAsyncResourceof the outer run.While it may not be especially clear, what this means is that multiple sets of context data are not stored and therefore accessible in descending async code. Only the data of the nearest run will be reachable. The
store.getStore(...)method is a very simply mechanism that only gets the current resource and grabs a symbol property off of it which stores the context data. WhatAsyncLocalStoragedoes internally is copy that symbol property onto everyAsyncResourceinstance created while in the lifetime of another, which is to say thosesetTimeout(...)calls create an internalAsyncResourcewhich copies its context data from theAsyncResourceinternal to thestore.run(...)method. No stacking of context data, just stacking of the underlying resource tree. In a sense, this means sync code will essentially have stacked contexts like this, but async code will never return to the outer resource and therefore never restore the conceptually outer context.- added a commit that references this issue
on May 1, 2021 github-actions commented
on Jun 27, 2026 on Jun 27, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Jun 27, 2026 github-actions commented
on Jul 28, 2026 on Jul 28, 2026 – with GitHub ActionsContributorMore actionsThis issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 120 days).
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.
What steps will reproduce the bug?
I expected the following program to work:
But it fails in the line after the marked
****. The reason I expected it to work is because I assumed thatenter/exithas the same "stacked" semantics asrun, in that entering twice and exiting once will give me the store of the first enter. Unfortunately, the store I get after the firstexitisundefined.How often does it reproduce? Is there a required condition?
Consistent. Always.
What is the expected behavior?
The program should pass without error. See above for why.
What do you see instead?
A failure of the code:
Additional information
This also fails in v14.15.1