Description
Version
v18.12.1
Platform
Darwin nfml-jonj5MM 21.6.0 Darwin Kernel Version 21.6.0: Wed Aug 10 14:25:27 PDT 2022; root:xnu-8020.141.5~2/RELEASE_X86_64 x86_64
Subsystem
async_hooks
What steps will reproduce the bug?
Run this repro:
const { AsyncLocalStorage } = require("async_hooks");
const assert = require("assert");
const store = new AsyncLocalStorage();
const unrelatedStore = new AsyncLocalStorage();
// the test passes if you comment this out
unrelatedStore.enterWith("unrelated");
// oversimplification of jest internals 😅
async function testHarness() {
// the test also passes if you enter synchronously, e.g. change to
// `store.enterWith("value")` or `await new Promise((resolve) => resolve(store.enterWith("value")));`
await Promise.resolve().then(() => store.enterWith("value"));
assert.equal(store.getStore(), "value");
}
testHarness();
How often does it reproduce? Is there a required condition?
The test fails every time, unless you tweak it as indicated in the comments, in which case it always passes.
What is the expected behavior?
It should behave consistently independent of other ALSes. It should either always propagate the inner async context, or it never should.
What do you see instead?
node:internal/process/promises:288
triggerUncaughtException(err, true /* fromPromise */);
^
AssertionError [ERR_ASSERTION]: undefined == 'value'
at testHarness (/Users/jonj/projects/jest-als-bug/src/simple-repro2.js:15:10) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: undefined,
expected: 'value',
operator: '=='
}
Additional information
This was originally reported as a Jest bug, but further debugging indicates it's an issue in Node.js. The basic summary is if you:
- Set an ALS store (either via
enterWith
orrun
) (e.g. in a test suite'sbeforeEach
) - Asynchronously set an ALS store in the course of resolving a promise (e.g. in another test suite's
beforeEach
) - The second store will be undefined in any async operations that key off that promise (e.g. the test code). If you omit step 1 or make step 2 synchronous the store value will be available.
I think an argument could be made that this is (mostly) working as designed since the store is being set in an inner async context and shouldn't apply to other operations in the testHarness
(see repro). But if that's the case, why does it propagate when there is no other ALS? Ideally the behavior should be consistent independent of the unrelated ALS, and the docs could probably be clarified a bit.
Here's a Jest repro that might be more representative of how an end-user could encounter this issue. Since Jest has no aroundEach
functionality, end-users might be tempted to try to set a store along these lines, which could then encounter context loss:
const { AsyncLocalStorage } = require("async_hooks");
const store = new AsyncLocalStorage();
describe("store", () => {
beforeEach(async () => {
store.enterWith("value");
});
it("the store should be set", () => {
// this passes, unless another store gets set 😬 (even a different ALS in another test suite!)
expect(store.getStore()).toEqual("value");
});
});
From a Jest end-user perspective the test runner code is a black box, so it would be nice if the ALS context propagated to those outer chained async operations. While there are workarounds like AsyncResource.bind()
, they may not be ergonomic or practical for end-users. That said, I could see that kind of propagation being problematic and having unintended consequences, so it might be better to make it never work (i.e. even in the no-other-ALS case), and make the docs a little more clear.