Use a top-level await, if supported by the current runtime
Asked Answered
F

3

6

Top-level await support was added to Node.js in 14.3.0 via --experimental-top-level-await and later to --harmony-top-level-await.

The Problem

I need to use a top level await in my ESM script file, if it is supported by the current Node.js runtime. And further, I need to set a boolean flag to indicate that the promise was successfully awaited at the top level.

An example of what I mean:

let topLevelAwaitEnabled;
try {
    await Promise.resolve(); // replaced with an actual promise
    topLevelAwaitEnabled = true;
} catch (ignored) {
    topLevelAwaitEnabled = false;
}
console.log(topLevelAwaitEnabled);

// carry on with the rest of the application, regardless of success or failure
// at some point, topLevelAwaitEnabled is checked to conditionally execute some code

If top level await support is enabled, this succeeds fine. However, if it is not supported, this will result in the following error during parsing and cannot be caught at runtime with a try/catch:

$ node test.js...\test.js:3
    await Promise.resolve(); // replaced with an actual promise
    ^^^^^

SyntaxError: await is only valid in async function

So the question is: How can I use a top level await if it is supported, without creating incompatibility issues with Node.js runtimes that do not support top level await (either no CLI flag was specified or simply no runtime support)?

If the answer is "it is not possible", I would like an explanation as to why this is impossible.


In the case I am actually committing an XY problem, the underlying issue is I need a top-level dynamic import.

Note: I am well aware that top level await is not recommended for a variety of reasons, however it is crucial for a specific functionality of my application and does not impose any issue with my use case. Alternatives will likely not suffice.

Attempts

I have tried the following methods, to no avail:

  • eval: I have tried replacing the await line with an eval("await Promise.resolve()"), in the hope the code was evaluated in the current context. Unfortunately, even if top level await is supported, this will result in the same error, as it does not seem to inherit the current context.
  • vm.compileFunction: Same issue was eval(), top level await is not supported.
  • vm.SourceTextModule: Evaluation is asynchronous and would need to be awaited at the top level to check if it is supported... which is a catch 22.
  • conditional execution of the await based on process.version and process.execArgv: The error during parsing - it never actually executes the code, so conditional execution is ruled out.
Fruit answered 3/11, 2020 at 20:54 Comment(3)
Is there a reason you wont use a .then here? If you say the issue is because you need a top level dynamic import, seems like a then instead of an await would work, no?Pedate
@ChaimFriedman I am implementing an ESM loader polyfill that dynamically imports other ESM loaders to emulate ESM loader chaining (not currently supported). The getGlobalPreloadCode() hook is unfortunately not asynchronous and is invoked immediately after my script is loaded. Since dynamic import is asynchronous, I am unable to delegate the getGlobalPreloadCode() call other ESM loaders, as they are not yet loaded. The remaining hooks are async, and I can await inside of the hooks.Fruit
This is clearly possible as node-fetch and test have done it. I think it can be done with require.resolve('node:test') or similar. Another way to go about it is to create a function that uses import, run it and see if it returns an error.Nomo
K
2

As far as I know this is not possible because the parser will simply error out. The compiler will not understand the await directive and will not complete its cycle. This is probably similar to using a word that's simply not a recognized keyword.

The closest you can get is using an anonymous function.

Klara answered 3/11, 2020 at 21:22 Comment(2)
That is rather unfortunate. If no answer is able to prove otherwise within a reasonable time frame, I will mark this as the accepted answer. In the case of other keywords, an eval() would be able to correctly verify the keyword is supported at runtime, since there is no concern over asynchronous behavior.Fruit
The only other option I can think of would be to use some sort of preprocessor to determine nodejs's version and overwrite your file with a backup using a top-level await or not as well as assigning the variable.Klara
P
1

Seems like you might be able to check the version of node being used at runtime like so process.version and then you can use also use process.argv to check for any flags passed in when starting the process.

Using the above methods, you can first check the version of node being used, and then if relevant, you can check for the needed flags.

Node docs on checking for CLI args.

Pedate answered 3/11, 2020 at 21:8 Comment(2)
Thanks for the quick response. Unfortunately this does not appear to work because the await will error regardless whether it is is conditionally executed with an if. I investigated this as an alternative, but ultimately it appears to result in the same underlying issue - the parser does not appreciate the await regardless of where it is positioned. It never manages to reach the code block or executes the code. If it did, this would be a perfectly viable solution.Fruit
Yup just tested my own theory after answering, and as you say the parser wont allow top level await to be there in the wrong version. This does make sense because node does not do 1 pass over your code and execute as it comes across your code. It will do at least one "compile" pass before actually running the code. If during this pass it encounters something it cant compile it will blow upPedate
X
0
(async()=>{await promise})()
Xenophanes answered 3/11, 2020 at 21:13 Comment(2)
This is not a top level await and will unfortunately not meet my use case.Fruit
huh.. i see, so... an await that wont work in normal async tabs?? interestingggggXenophanes

© 2022 - 2024 — McMap. All rights reserved.