Possibilities for Other Syntax?
fixiabis opened this issue ยท 17 comments
I have referred to the content mentioned in #186, #15.
Is it possible to express it using the following code?
const resource = getResource() finally dispose;
const asyncResource = (await getAsyncResource()) finally await dispose;
Not entirely certain about potential concerns, but would the introduction of a finally dispose
/ finally await dispose
syntax like this help improve the situation?
Not a fan of current disposal syntax at all especially the async one, it looks very confusing.
Especially the await using
syntax is extremely confusing given that it introduces a completely different meaning to an existing await
keyword. I really hope that this gets reconsidered before mass adoption
I would much prefer async using
instead of await using
. I feel like await
implies a promise that is being resolved but that is not the case.
This seems very confusing to me:
await using connection= await db.connect()
The first await
on the right is resolving a promise, blocking execution. The second await
is not blocking execution.
Compared to:
async using connection = await db.connect()
To me this conveys a bit clearer that the left side will be handled asynchronously during/after execution
This idea is inspired by TypeScript as
keyword. Therefore, in my imagination, the following code would also be feasible:
(getResource() as DisposibleResource finally dispose); // returns value, would be dispose when leave scope
I've been playing with resource management quite a bit using it's inclusion in TypeScript.
Even after building some familiarity and muscle memory the two await
keywords in an asynchronous creation of an AsyncResource
feels wrong. I often forget to place one of the two required await
keywords and am only pushed back onto the happy path thanks to TypeScript's language server.
I think the await using
syntax is fundamentally at odds with the rest of the naming in the proposal: This syntax is to indicate that you're using an async resource, not an await resource.
To align the syntax with my intuition and a prose description of what's going on, consider:
I would like to use an async resource called
foo
that I've obtained by awaiting the result of this function call.
using async foo = await thisFunctionCall();
I think the key difference here is that we're 'using an async resource' which translates directly to the syntax using async
. When using a sync resource, we are just using
it.
I had the same experience: I read the following line here and did not understand it.
await using file = await openFile("dist/test.txt", "w");
await
doesnโt feel like a modifier to me. Either of the following two lines would have been easier to understand:
async using file = await openFile("dist/test.txt", "w");
using async file = await openFile("dist/test.txt", "w");
To add to this, the prefix of await
currently guarantees that execution will yield to the microtask queue. If the current syntax were preserved, that rule would no longer hold and every function using the syntax will be harder to understand.
Are there technical reasons that motivate the current async-prefixed syntax? Some parsing / backtracking trickiness? Could there be a path where the current syntax introduced in TypeScript remains supported over a deprecation period and an 'improved' syntax coexists with it?
I had the same experience: I read the following line here and did not understand it.
await using file = await openFile("dist/test.txt", "w");
await
doesnโt feel like a modifier to me. Either of the following two lines would have been easier to understand:async using file = await openFile("dist/test.txt", "w"); using async file = await openFile("dist/test.txt", "w");
We discussed a number of syntax alternatives in plenary, including await using ...
, using await ...
, async using ...
, and using async ...
, and in the end we settled on await using ...
which is what we advanced to stage 3.
await using
has several advantages to it:
- It is consistent with C#'s syntax, which was the prior art that
async
/await
was originally derived from. - Since it comes before the
using
keyword, it is easily recognized when a reader visually scans an indented code block. - Not only implies an
await
occurs, but by having it prefix theusing
keyword it indicates that what is awaited is the effect of theusing
keyword, not the initializer.
I was opposed to async using
as the async
keyword does not imply an await
will occur, it implies a function allows the use of the await
keyword. Repurposing it for use with using
would be inconsistent with the language.
await
doesnโt feel like a modifier to me.
await
has also been a modifier since the introduction of for await
.
Especially the
await using
syntax is extremely confusing given that it introduces a completely different meaning to an existingawait
keyword. I really hope that this gets reconsidered before mass adoption
I disagree that it gives a different meaning to await
. There are two cases of await
in JS today: AwaitExpression, and for await
. The meaning here is closer to for await
, which has an implicit await
per iteration. And, just like await using x = await y
, there are times where you will have to write for await (const x of await y)
in code today, because for await
does not implicitly await the expression you are about to iterate.
This idea is inspired by TypeScript
as
keyword. Therefore, in my imagination, the following code would also be feasible:(getResource() as DisposibleResource finally dispose); // returns value, would be dispose when leave scope
This is something that would never reach consensus. It was made clear early on that anything that would introduce end-of-block cleanup needs to be declared at the statement level, not at the expression level.
To add to this, the prefix of
await
currently guarantees that execution will yield to the microtask queue. If the current syntax were preserved, that rule would no longer hold and every function using the syntax will be harder to understand.
I think this is a slightly incorrect interpretation of await
. As I've said in another comment, for await
does not precisely guarantee execution will yield to the microtask queue, but rather that it will yield at the beginning of each iteration, which coincidently is also the end of each iteration. await using
also yields to the microtask queue, but at the end of the block.
Are there technical reasons that motivate the current async-prefixed syntax? Some parsing / backtracking trickiness? Could there be a path where the current syntax introduced in TypeScript remains supported over a deprecation period and an 'improved' syntax coexists with it?
The technical reason is that you need to declare that an await
will occur during cleanup at the time the resource is initialized, as that determines whether to use [Symbol.dispose]
vs [Symbol.asyncDispose]
for cleanup. That indication needs to be clearly marked at the front of the declaration, and at statement level, so that it can be readily recognized by code reviewers reading code containing an await using
declaration.
If it doesn't fly for some reason, I much prefer
async using ...
thenusing async
toawait
, per @ggoodman's argument.
We have discussed all of these options in plenary over the course of multiple meetings and settled on await using
. We conducted a number of informal polls, both publicly and internally within several TC39 member organizations and found that the percentage of respondents that favored async using
and the percentage of respondents that favored await using
were almost identical, with a slightly larger number favoring the latter. Whichever syntax we chose, this would be new syntax that users would need to educate themselves on before using. Now that this proposal has reached stage 3, I don't expect the syntax to change without a very strong reason above and beyond what's already been discussed in plenary.
Closing as this merely rehashes a settled debate in plenary, which already reached consensus in await using
.
Thanks for taking the ideas into consideration. Can't wait for this to be available in engines!