Repository navigation
VM ESM with dynamic imports resolves promises before linking is complete #33216
Description
- Version: v14.1.0
- Platform: Darwin Simens-MacBook-Pro.local 18.7.0 Darwin Kernel Version 18.7.0: Mon Feb 10 21:08:45 PST 2020; root:xnu-4903.278.28~1/RELEASE_X86_64 x86_64
- Subsystem: VM, ESM
What steps will reproduce the bug?
See https://github.com/SimenB/node-vm-esm-promise-awaiting
Run node --experimental-vm-modules index.js to reproduce.
How often does it reproduce? Is there a required condition?
Almost every time. There seems to be some race condition in there. I've added a manual timeout in there - tweak its numbers to see different behavior. In the code the timeout is 1 second and then one of the tests fail almost every time. Increasing the timeoyt will often make both tests fail
What is the expected behavior?
All linking and execution should happen before the promises resolve.
What do you see instead?
The promises resolve, so the internal state of completed is set to true, which later throws.
Additional information
The reproduction is adapted from a bug report to Jest (jestjs/jest#9430 (comment)), so apologies if it looks a little wonky. I've tried to emulate sorta what happens under the hood in Jest down to solely node core modules.
Note that I might very well have gotten some semantics wrong in the dynamic linking, so please tell me if I'm doing something really dumb in the reproduction (except having no module cache at all) - I've probably made the same mistake in Jest's implementation. Main difference is that the calls will linger - In jest these would all be inside test() calls or some such that we execute later. I don't think it impacts the reproduction much though - the promises resolve seconds before linking complete, so I don't think it's necessarily tied to dangling promises. I might very well be wrong though.
For some extra context if it's helpful, here's Jest's implementation: https://github.com/facebook/jest/blob/7a3c9977847cc9fafed6f6662289f3c35e44e0c6/packages/jest-runtime/src/index.ts#L323-L456
It's spread a bit around, but those are the essential bits where we use the VM APIs.
Activity
GeoffreyBooth commentedon May 2, 2020
cc @devsnek
devsnek commentedon May 2, 2020
The example given looks sufficiently contrived/broken (multiple linker functions, multiple calls to evaluate, etc) that I am not convinced there is a bug with node at this point. If a simpler reproduction can be provided I will gladly take a look.
SimenB commentedon May 2, 2020
That doesn't bode well for my current implementation in Jest then as I have pretty much the same approach there 😅 I can remove the
evaluatecall in the linker, but not thelinkcall, then it fails withModule status must not be unlinked or linking. There's no example in the docs usingimportModuleDynamically, so I might very well be using it wrong. Am I not supposed to reuse a linker function as callback forimportModuleDynamically?devsnek commentedon May 3, 2020
@SimenB here's an example loader that might help:
While you can evaluate and link each layer of the graph manually, it is much simpler to just call
linkandevaluateat the top. Them.statuschecks are needed in case the static entry is dynamically imported.SimenB commentedon May 3, 2020
Thanks @devsnek, that's cleaner indeed! And it does fix the error in my original reproduction as well. However, making the same change in Jest doesn't fix the issue there.
I've now spent some more time on it, and I'm now able to see the same error I can see in Jest - which is that it seems like the linking tries to link modules from different contexts, or something? I've changed the repo in the OP to have a fake super-dumb test framework and added a "teardown" when a test complete (setting
completedtotrue). I'm seeing the error about 1 in 10 times, while in Jest it's reproducible 100% of the time, but hopefully it's enough. I moved theentryandloadfunctions inside therunTestfunction to easily have separate caches and contexts per test, but it should be an easy refactor to just pass those as arguments and keep them at the top level.Example of the error.
I'm really not sure what the bug is, but there are 3 things wrong with the behavior that I can see
goodbye.test.js, which has already completedhello.js, which it should never dohello.test.js:4:18which is unexpected, as that's not the test we're running.EDIT: Just pushed another commit which removed the timeout stuff I added, and the test now fails almost every time I run it
EDIT2: Pushed yet another commit to bring it even closer to Jest - a runtime class which holds the state instead of closures
devsnek commentedon May 3, 2020
@SimenB you have a TOCTOU here because of the async file op: https://github.com/SimenB/node-vm-esm-promise-awaiting/blob/d7e5baf9927e5400432575ae8de55253c24a7e78/index.js#L39-L57
targos commentedon Jan 30, 2021
Is this still an issue?
SimenB commentedon Jan 30, 2021
Yes, but I believe #36351 might be the underlying reason. Not 100% sure - would be interesting to attempt to reproduce this after that issue is fixed. I haven't found the time to dig into this yet, but I'm hopeful I'll find the time in the not too distant future (I assume a bunch of modules will start to be ESM only once Node 10 is EOL, at which point good support in Jest will be very much needed)
SimenB commentedon Mar 7, 2021
I updated the repo now so it still works on newer versions of Node (the TLA PR removed the result from
evaluateso had to move things around a bit).@devsnek I don't quite understand why the TOCTOU concern you mention is an issue.
helloshould never accessgoodbye(and vice versa), so it's not like a cache miss and a subsequent "duplicate" module should mess with anything, is it?I tried adding a super simple mutex, but that is never triggered (i.e. the same module is not loaded more than once in the same "test run"). You can remove the module cache (or throw on module cache hits) and we still get the same error. One "test" never accesses the same module twice, so the cache shouldn't matter. Both tests access the same module, but they should be independent since they're evaluated in different contexts.
I agree removing the async
readFile(e.g. replacing it with areadFileSync) fixes the error in the reproduction, but seeing as module resolution can be async I'm not sure how that's the underlying issue? Could you perhaps show me how you'd structure async access to the "same module" (i.e. same identifier and content, but different instances ofSourceTextModuleand using different contexts) in parallel?It looks to me like there's some sort of unwanted cached module somewhere, so the wrong thing runs, do you think it might be the same issue as #36351?
devsnek commentedon Mar 7, 2021
@SimenB the issue is this:
SimenB commentedon Mar 7, 2021
Right, but in this case 1. never hits - the same module is never loaded twice (from the same test). So I don't think that's an issue in practice?
But I see you're saying step 2 is the issue, so maybe the cache part doesn't affect things. But does that mean we cannot run
importModuleDynamicallyin parallel? I.e. we need some mutex/lock in there. There is only oneimport()call per test, so even that shouldn't be an issue I think, unless we cannot runimportModuleDynamicallyin parallel within the same node process at all? That would seem like a bad limitation, so I assume I'm still missing something 😅KernelDeimos commentedon Sep 8, 2025
Hi, I'm trying to figure out the state of
vm.Moduleand what I can do to help get it working. I tried this example and it looks like this is working - after several dozen runs I get this output every time:Sometimes "goodbye" appears first but I can see that's intended. @SimenB suggested the underlying cause is #36351 and this issue is closed now, so it seems absolutely possible to me that this issue is fixed and hasn't been closed yet. I won't close it myself since I'm not a nodejs contributor (yet) and I might be missing something here - would appreciate if somebody can take a second look.
github-actions commentedon Jun 27, 2026
This 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.
SimenB commentedon Jul 8, 2026
Still relevant, but possibly better tracked in #62720? No sure
github-actions commentedon Oct 8, 2026
This issue has been marked as stale due to 90 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.