Skip to content
This repository has been archived by the owner on Sep 8, 2021. It is now read-only.

Does it support async-await? #7

Closed
zhanzhenzhen opened this issue Dec 1, 2018 · 11 comments
Closed

Does it support async-await? #7

zhanzhenzhen opened this issue Dec 1, 2018 · 11 comments

Comments

@zhanzhenzhen
Copy link

Does it support async-await?

@rbuckton
Copy link
Collaborator

rbuckton commented Dec 2, 2018

At the moment, no. The only way I could see that be feasible is if the ability to await is determined by the container of the class (i.e. inside of an async function).

@rbuckton
Copy link
Collaborator

rbuckton commented Nov 6, 2020

Currently both await and yield are disallowed inside of the block. This means that await is not permitted even when evaluating the class at the top level of a module where top-level await would be allowed. We may chose to revisit this as a follow-on proposal at a later date.

@bakkot
Copy link

bakkot commented Nov 15, 2020

@rbuckton What's the reason for this restriction? It's surprising to me.

@bakkot
Copy link

bakkot commented Jan 14, 2021

@rbuckton ping^

@rbuckton
Copy link
Collaborator

To reduce complexity. IIRC, we've discussed in committee the possibility of opening this up later, and we've added (and are adding) restrictions so that we can open this back up in the future. Currently, allowing await in a class static block is a possible refactoring hazard (i.e., you have the class at the top level of a module, but then move it inside of a function forcing that function to become async, etc.). Also, a static {} block acts more like an IIFE, not a regular Block (it starts a new var environment, gets added to the call stack, etc.). We may decide, in the future, that a static block that contains async code must require the static block itself to be marked with async, etc.

There are a number of directions we can go for this that we don't have to decide now, so I've focused on shipping a MVP solution that we can expand upon in the future. Multi-step static initialization with access to private state is something I'm seeing is needed now since private fields is already shipping in NodeJS. I've already seen in-the-wild examples of people abusing computed property names to try to achieve this, and that approach is clunky and hard to read or maintain. Supporting await and yield are such narrow corner cases that I'm hesitant to delay the whole feature until we've decided on how to handle them. yield is already reserved in strict mode (and class bodies are always strict), and we've added semantics to ensure await is reserved so that we can make these decisions after the community has an opportunity to use the feature as-is and provide feedback.

@bakkot
Copy link

bakkot commented Jan 14, 2021

Hm, OK. I guess I didn't imagine there was any question about what the semantics would be.

Actually, I still can't imagine that. I thought the whole idea of this feature was that it allowed you to take code that was executing outside of the class body and put it inside of the class body with basically identical semantics but now with visibility of private fields. So what semantics other than "exactly what the same code would do immediately outside of the class body" could there be? Why would we want to make it so that that only worked if the code for initialization happens to be synchronous?

@rbuckton
Copy link
Collaborator

This is already a Stage 1 proposal about async class initialization. That proposal could have ramifications on the design of how something like await should work in a static block (especially if it requires the introduction of async on either the class or the constructor).

[...] I thought the whole idea of this feature was that it allowed you to take code that was executing outside of the class body and put it inside of the class body with basically identical semantics but now with visibility of private fields. [...]

Its not quite that simple. As I've said before, a static {} block behaves more like an IIFE run inside of the class body than a regular block. This keeps the variable scope local to the body so things like var can't escape and pollute global scope. This also changes what you resolve for bindings like arguments and this. The declarative environment and the this binding change when evaluating a static {} block. There is no other place in ECMAScript where that happens and we carry over yield, await, return, break, or continue. We've also disallowed return and non-local break and continue to preserve these boundaries and avoid confusion.

Rather than thinking of static {} as just another Block moved inside of a class to give it access to private state, consider it more like a Java Static Initializer or C# Static Constructor (which are the prior art referenced in the explainer).

I'm not saying I'm opposed to ever adding support for await or yield, which is specifically why I've added restrictions. However, I'm not currently convinced that adding them wouldn't make the feature (and the language) far more complicated by blurring the boundaries we've already established.

You can still achieve async initialization without await in static {}, though it does add complexity (and far less complexity than not having static {} at all):

// promise to await in the module body
let cPromise;

// local binding so we don't eagerly export an uninitialized `C`.
let C_ = class C {
  ...
  static {
    // non-async init code
    this.x = ...;

    // async init code using async IIFE...
    cPromise = (async () => {
      await ...;
      this.y = ...;
    })();
  }
}

// wait for the async init of `C` to complete before making the class available as an 
// export (so circular references don't result in access to an uninitialized `C`)...
await cPromise;

// export the initialized `C` (exported binding is initialized only after `C` 
// has completed its own initialization).
export let C = C_;

@bakkot
Copy link

bakkot commented Jan 14, 2021

As I've said before, a static {} block behaves more like an IIFE run inside of the class body than a regular block.

That's a choice this proposal makes, not an inherent part of the design. Personally it seems like a surprising choice.

consider it more like a Java Static Initializer or C# Static Constructor

That doesn't really help me, since I think of static initializers in those languages as being blocks which are executed in the context of the class body.

I'm not currently convinced that adding them wouldn't make the feature (and the language) far more complicated by blurring the boundaries we've already established.

Mm. To me it feels like leaving them out is adding complexity, not removing it. Now consumers have to know this extra fact about static {} blocks, instead of just treating them like the blocks they appear to be, and have to write workarounds for natural use cases like asynchronous initialization.

@rbuckton
Copy link
Collaborator

You've not commented on the part I wrote about how nowhere else in the language do we change this and allow await/yield from the outer scope to pass into the new scope. I've often said that I would have written this as static() {} or static constructor() {} if those hadn't already been legal ES. Despite the lack of parens, static {} is functionally equivalent to an unreachable static method that is evaluated at the end of ClassDefinitionEvaluation (with the caveat being that arguments is unreachable). Yes, that is a design decision this proposal has made, but it was made to maintain consistency with other aspects of the language. Making static {} "just another block with private name visibility" has a whole other set of consequences (var hoisting, the inconsistency of return, break, and continue, etc.).

@bakkot
Copy link

bakkot commented Jan 14, 2021

You've not commented on the part I wrote about how nowhere else in the language do we change this and allow await/yield from the outer scope to pass into the new scope.

Well, yes, but there's just not that many different contexts in the language at all. It doesn't seem like much of an inconsistency to me.

(That said, I'd be happy for this, arguments, etc to just mean what they do in the outer scope, exactly as happens for computed property names in class bodies.)

Making static {} "just another block with private name visibility" has a whole other set of consequences (var hoisting, the inconsistency of return, break, and continue, etc.).

Yup. var should hoist, that sounds right to me. That's strictly more useful, and is what I would assume would happen given that there is no visible indication of a function boundary. (An intuition at least DE and SYG shared the last time we discussed this, I believe.)

@parzhitsky
Copy link

parzhitsky commented Sep 7, 2021

Well, we could have had static {} block and async static {} block, just like we have function () {}, which don't inherit parent context's async-ness, but can become async themselves by adding async function () {}:

class Thing {
  static {
    console.log(1);
  }

  async static {
    console.log(2);
  }

  static {
    console.log(3);
  }
}

// logs: 1, 3, 2

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants