-
Notifications
You must be signed in to change notification settings - Fork 29.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why don't CJS modules support "top level await"? #21267
Comments
is there any safe way to do this? even the entry module can be required and we can't detect that so it would just break people expecting require to not return a promise. on a more theoretical level I think features hinged on cjs being wrapped in a function are generally a bad thing |
Having this happen in a hidden way will lead to the worst, most confusing debugging scenarios ever. |
Is the idea to basically sugar this: module.exports = new Promise((resolve, reject) => {
resolve(true);
}); into: module.exports = await true; Doesn't seem that out-of-line to me. |
I do not think there is any safe way to do this for CJS. The only way to achieve this would be to wrap everything with an async function. This means that all modules will return a Promise.
For what it's stated before, this would not be possible. Promises always resolve asynchronously. I'm -1 for the time being. |
How does this idea differ from using an async IIFE when you want async behavior? module.exports = (async () => { ... })(); |
Top Level Await is currently targeting the module goal only, so it will not get adopted by default into CJS, and introducing it would likely introduce weird ecosystem fracture. Outside of this, I've done some experiments with changing our IIFE with an AIIFE... and it broke everything. CJS does expect execution order and that it happens synchronously. This seems like something better left to userland imho. |
Also, I am pretty sure this is impossible to do in a way that doesn't explode everything. Feel free to prove me wrong but I've also done a lot of thinking about this in regards to async-ness with es modules in the past. |
@Fishrock123 supporting top-level await would require pre-processing the AST to know about them and then wrapping in an |
@tbranyen it’s a refactoring hazard if merely adding |
Doesn't it dramatically change any API? |
Depending on how TLA is specced. If TLA is only allowed in modules without exports - which is one of the possibilities - then it wouldn’t impact any APIs at all. |
Forgive me if this is off-topic, and I feel like this must’ve been discussed elsewhere (perhaps ad nauseam) but in the discussion around top-level await has there been any thought given to a way to wrap async functions to make them synchronous? I’m thinking of Meteor’s const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
wait(2000).then(() => console.log('waited 2 seconds')).catch(err => console.error(err));
console.log('hello!'); This prints import { Promise } from 'meteor/promise';
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
Promise.await(wait(2000).then(() => console.log('waited 2 seconds')).catch(err => console.error(err)));
console.log('hello!'); This waits two seconds, prints As far as I know this isn’t possible in Node today, other than by pulling in Fibers (as Meteor does), which I’m calling a cheat because it’s essentially hacking Node. But might there be a place for Anyway there’s probably a better thread for this, I only bring it up here because if |
is Promise.await here just while (promise.state == pending) {
uv_run(UV_RUN_ONCE);
runMicrotaskQueue();
} |
Basically yeah, you'd |
@GeoffreyBooth no, that's not something the language would ever allow - node allows it with C++ modules, but that's not really a pattern we should be emulating. Separately, TLA wouldn't block, so a blocking API wouldn't be equivalent. |
To be clear, I am not suggesting we make something that is compatible with TLA, |
Blocking in sync i/o is not the same thing as blocking in any async action. Also, adding something like that to CJS - if not supported in the browser - might break a ton of common webpack/browserify workflows. |
@ljharb I suspect this sort of thing is probably the primary reason why it shouldn't be pursued. Although browserify/webpack could easily introduce tlaify/tla-loader to support this and probably will independently of Node landing it. |
@ljharb good point about webpack compatibility. + CJS is frozen. |
I think it is still good to think about this sort of thing just for thought experiments. In particular I find mutating await null;
// any defered action works
setTimeout(() => { module.exports = {ok: true} }); Since the idea would be that With promises we won't be able to change the value once we resolve it. On that note, errors also have some questions and in particular what happens on errors: await null;
throw new Error(); And what happens when you require the module a second time. Does it cause a reload like CJS currently does, or does it treat this a a success since it was able to return a All of these lingering questions make for tough design decisions and we can probably just let users wrap things in async IIFEs so they can decide on those behaviors. If those behaviors have clear winners and the IIFEs are too burdensome to maintain we can revisit this I think. |
@bmeck IMO it'd work the same as this: // cat test.js
module.exports = new Promise(resolve => {
setTimeout(() => {
module.exports = { ok: true };
});
resolve();
}); // cat test2.js
const test = require('./test');
console.log(test);
test.then(resp => {
console.log(resp);
console.log(require('./test'));
delete require.cache[require.resolve('./test')];
console.log(require('./test'));
}); λ node test2.js
Promise { undefined }
undefined
Promise { undefined }
Promise { undefined } |
@tbranyen then |
@bmeck I'm just showing that this thought experiment exists without top-level await. To answer your question: module.exports = (async () => {
setTimeout(() => {
/* We already know this won't work */
/*module.exports = { ok: true };*/
});
// Would be how you set the "exports"
return await { ok: true };
})(); Top level return already (confusingly) exists in CJS. Edit: w/ top level await... return await { ok: true }; |
@tbranyen thats very different from how I would have thought your example to work, interesting. |
It surprises me that there have been so many comments on this thread already, but no one has mentioned dependency cycles yet. Then again, someone always has to be the first to bring cycles into any discussion of module systems, so I guess it's my lucky day. 💫😭 In addition to all the other reasons asynchronous CommonJS is a non-starter, there's this: The CommonJS policy for handling dependency cycles fundamentally assumes synchronous module evaluationThe following argument is fairly subtle, but please bear with me. There are folks commenting on this thread who are literally designing and implementing Node's next module system, so I really sincerely hope they (in particular) will take the time to parse, instantiate, and evaluate this argument. I too regret the length of this post, in case you were wondering. What is the CommonJS policy for handling dependency cycles?If you In CommonJS, the rules are clear. When you
Hopefully this logic is familiar to everyone. Why does this policy work?I claim this policy works pretty well for CommonJS, but only because CommonJS module evaluation is synchronous. Without synchronous evaluation, CommonJS has no good options for coping with dependency cycles. Case 3 above is the problematic one. When If If Returning immediately is a reasonable policy for CommonJS because there happens to be only one reason why a module you're attempting to Take a moment to appreciate this subtlety: if there were any other reasons why a module might be suspended in the middle of its evaluation, then we could not assume that a dependency cycle was to blame, so returning Counting reasons is a pretty weird logical exercise, because there's a big difference between zero, one, and more than one reasons. Why is there exactly one reason in this case? Simply put: the only way for a CommonJS module to suspend itself and call out into other code (that might in turn call In other words, synchronous evaluation is essential to CommonJS having a reasonable policy for coping with dependency cycles. What would happen to this policy if CommonJS module evaluation became asynchronous?By definition, a module whose evaluation is asynchronous can be suspended for any number of reasons in the middle of its evaluation. Here are two such reasons:
In an asynchronous module system, the second possibility is much more common than the first, and the ability to conceal asynchronous work as an implementation detail is a big part of why async modules are attractive in the first place. However, a module system still needs a policy for what to do when a suspended module gets imported. When it comes to ECMAScript modules, we have some room to invent new policies, but CommonJS does not have that luxury. If CommonJS continues returning However, if CommonJS returns If CommonJS instead decides to wait for Why is that no better than synchronous CommonJS?If CommonJS always returns This is literally no different from what we have today: synchronous CommonJS modules can synchronously populate their This is not such a terrible system (CommonJS works!), but making the module system asynchronous does not improve the situation at all. It just makes a false promise to module authors that they will (only sometimes) be able to conceal asynchronous work. Couldn't we just detect whether there's a dependency cycle somehow?If the module you're trying to import turns out to be currently suspended in mid-evaluation on an
This question is equivalent to the Halting Problem (as I think @benjamingr noted recently in another thread), so there is no fool-proof way to decide the answer, in principle. While I am sympathetic to any close approximations that only get the answer wrong in certain easy-to-spot situations, I have not come across any good approximations. I would challenge all of you to spend some time chipping away at this academically unsolvable problem, because the advent of ECMAScript top-level Thanks for making it this far! |
Just a note - we can support module top level await in CJS in the way Gus suggested in #21267 (comment) . It does not require asynchronous resolution as in calling code that has made a |
That is not what we are proposing though and: array.forEach(elem => {
... await ...
})) Wouldn't work (sorry!), it's just top-level and not anywhere else, like ESM top level await in this regard. (As a side note - I don't consider @ljharb a "gatekeeper" in any way, I consider him a useful source of ideas and a colleague. I have the same consideration to other TC39 members, in general - communication with TC39 has gotten a lot better over the past year. Also, ljharb is contributing a lot to Node regardless of his involvement with standards bodies and I think the opinions he expresses are his own rather than TC39's) |
We can pick and choose restrictions on where |
@benjamn please do feel free to open a new issue about As a side note, I think the important aspect of @ljharb's comment about webpack/browserify is that there is no 'obvious' way for them to implement this (since they can't literally 'block') without doing heavy AST transforms. I think that the idea is definitely worth exploring regardless but it's an important point. I'll let discussion happen for a few days and then reach out to the webpack team to understand how hard/easy this is to do. |
To be clear, I share @ljharb's doubts that any of this is feasible for transpilers and/or module systems that do not have access to a native threading system. I was referring to his comment #21267 (comment), where he signaled clear disapproval about adding native fibers/coroutines to JavaScript. Here's a recent Babel thread where they seem to have decided they will not attempt to implement a transform for top-level Also, for what it's worth, I did not intend any negative connotation in the word "gatekeepers," though I think it is entirely fair to refer to TC39, collectively, as keepers of the JavaScript gate, so to speak. |
So I don't know if this helps with the conversation at all... but I am explicitly -1 on changing anything in CJS to support TLA. It was an explicit decision to target only the Module goal for TLA. These types of changes are too broad and too fragile. IMHO TLA being only in the module goal is a very compelling reason to upgrade 😉 |
Not to beat on a dead horse, but isn’t there some value in providing a way to wrap an async function to make it synchronous? In the general sense, not specific to the issue of top-level @ljharb I agree it shouldn’t be called Anyway I don’t feel strongly about this, just trying to propose a solution. I agree with @MylesBorins that it would be nice to give people a reason to upgrade to ESM 😄 |
@GeoffreyBooth if you want to see what an api like that looks like -> https://github.com/devsnek/syncify its worth noting there's pretty much no completely safe way to make this. its just not intended to exist. |
Can we please take the discussion of a way to make code "sync" to another issue? Feel free to open a new one. |
Can you please elaborate on this? I'm still waiting on #21267 (comment) Note that I'm not asking "in general", just specifically for the top level await use case. |
@benjamingr what I would describe as sorta "undefined behaviour." like sockets will randomly close, microtask timing gets messed up because the loop isn't running to completion, etc. just little random things that add up into this big ball of unstable. for people who opted into TLA, it would be like node had just gotten a bunch of new bugs. I also think but am not positive that because the loop is in c++, (until node 10ish when they moved marking to another thread) v8's gc will be blocked until the promise is resolved. it also greatly slows down the resolution of the promise as it basically has to wait for the fake event loop to proceed some unknown number of times (in my test you'll notice I tested waiting 1000ms as >= because it frequently comes out ~150ms later) |
@devsnek thanks, that's a lot of motivation for not doing this "head on". Note that the suggestion here isn't to expose a |
Basically, it would let people do:
And expose the capability outside without having to use a third party package. Risky. |
The most common (if not only) real usecase for the top level await with CJS that I could think of is with using it at the entry point which does not export anything. So, would allowing await in CJS files that do not export anything do? We can ensure that by blocking writes to I.e., on
That would not be a breaking change (for users who are not monkey-patching I also think that it would solve dependency cycles issue, as async modules would not be able to export anything at all and would be just scripts. Debugging should be easy if any attempt to export things via |
That restriction makes sense iff the same restriction holds in ESM, imo. I suspect it would obviate a number of problems. |
@ljharb, yes, taking the same approach for ESM top level await could be also a good idea. I do not know why would anyone want to export things asynchronously in ESM (i.e. not immediately after loading imports) — that would just overcomplicate things. The usecase that I see for top level await is for using it in entry points i.e. files that do not export things. That could probably be relaxed a bit, though from «block exports from files that use top level await» to «block exports after the first await»:
That will allow to write files that export things at the start and then use await at the bottom do launch something asyncrhonous. Or CJS files that have an Not sure if that is needed, though. Imo, it has some potential in creating strange dependency issues, but nothing more than what we already have with asynchronous assignment to |
Should this remain open? Or is this a conversation that has run its course and can be closed? |
I am closing this since there was no activity for over one year. Please reopen in case this should be continued to discuss. |
What is the difference between CJS and ESM's running env? CJS loads js module from local file system, while ESM needs perform a async network fetch first, so i guess it's the root cause of TLA design. However, if the points are how to let CJS support async modules via TLA-like extensions, i thought this misled the way. Why not separately build 2 versions of a module? ESM/CJS bundles can both bundled from a single TS source, with some conditionals supported by bundlers... |
It looks like the deasync library would allow synchronous import of esm modules. How about adding |
deasync is awesome! There's a newer deasync package with modernized code including type definitions and Promise support: https://www.npmjs.com/package/@kaciras/deasync const { awaitSync } = require("@kaciras/deasync");
module.exports = awaitSync(import('./some/es/module.js')) It doesn't seem to work on my Apple silicon though. Seems to lock up when I tested in a VS Code extension. A built-in solution in Node.js would be really nice. |
We can support
async/await
in our CJS modules and allow people to use top levelawait
.I saw quite a bit of people asking for this but I haven't seen much discussion about it.
Are there any downsides to this?
cc @addaleax @apapirovski @BridgeAR @MylesBorins @petkaantonov @spion @MadaraUchiha (maybe we need a nodejs/promises team?). Also cc @nodejs/modules in case anyone from there has an opinion to contribute :)
The text was updated successfully, but these errors were encountered: