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.
concision
  • 6,029
  • 11
  • 29
  • 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? – Chaim Friedman Nov 03 '20 at 21:16
  • @ChaimFriedman I am implementing an [ESM loader](https://nodejs.org/api/esm.html#esm_experimental_loaders) polyfill that dynamically imports other ESM loaders to emulate ESM loader chaining ([not currently supported](https://github.com/nodejs/node/pull/33812/)). 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. – concision Nov 03 '20 at 21:22
  • 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. – Ray Foss Nov 11 '22 at 20:08

3 Answers3

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.

Technoh
  • 1,606
  • 15
  • 34
  • 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. – concision Nov 03 '20 at 21:25
  • 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. – Technoh Nov 03 '20 at 21:54
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.

Chaim Friedman
  • 6,124
  • 4
  • 33
  • 61
  • 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. – concision Nov 03 '20 at 21:11
  • 1
    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 up – Chaim Friedman Nov 03 '20 at 21:13
0
(async()=>{await promise})()
concision
  • 6,029
  • 11
  • 29
The Bomb Squad
  • 4,192
  • 1
  • 9
  • 17