proposal-async-do-expressions
proposal-async-do-expressions copied to clipboard
implied `return` or `return await`?
Consider something like this:
let res = async do {
fetch("https://some.url");
}
The return value (here assigned to res) from a do async { .. } block is always a promise, right? And in this case, that promise will be fulfilled with the result of the promise that comes back from the fetch(..) call?
So is it appropriate to think of the block as sort of like a function passed to a then(..) in a promise chain, in that anything returned from the block, promise or non-promise, is "lifted to" (subsumed) by the return promise from that outer block expression?
Or is it a better mental model to think of it as an async function body?
What about this:
let res = async do {
try {
fetch("https://some.url");
}
catch (err) {
// ..
}
}
Is it as if there was an implicit return await there before the fetch(..), such that a local promise rejection is "caught" like it is in an async function? Or is it more just an implied return, such that our catch(..) WILL NOT catch promise rejection, and instead outer res will just adopt that rejected state?
If it's the former, does that unnecessarily unwrap the exception in a bare promise result (that's rejected) like the snippet above (in the case of there being no surrounding try..catch), so that the unhandled exception is then thrown right back into the outerres promise?
If it's the latter, and there's no automatic unwrapping, and you do await fetch("https://some.url") so that you can catch any exception, is that resulting in unnecessary promise fulfillment unwrapping in the non-exception case, where a normal fulfilled value is unwrapped, and then immediately re-wrapped in the outer res promise?
I think "async function body" is the right model. There is no implicit await at all, but the result of the async do expression is Promise.resolve()d, just like the result of an async function invocation is.
So is it appropriate to think of the block as sort of like a function passed to a
then(..)in a promise chain, in that anything returned from the block, promise or non-promise, is "lifted to" (subsumed) by the return promise from that outer block expression? Or is it a better mental model to think of it as anasync functionbody?
I'm not clear on what differences this would imply, so I'm not sure how to answer this.
Is it as if there was an implicit
return awaitthere before thefetch(..), such that a local promise rejection is "caught" like it is in an async function? Or is it more just an implied return, such that ourcatch(..)WILL NOT catch promise rejection, and instead outer res will just adopt that rejected state?
There's certainly no implicit await, and concretely if fetch results in a rejected promise in your example that will not be caught by the catch (at least as currently proposed).
In an async function the rejection is only caught if there's an actual return await in the async function, yes? Not just a return? So, yes, it's better to think of it as an implicit return, rather than a return await.
If it's the latter, and there's no automatic unwrapping, and you do
await fetch("https://some.url")so that you can catch any exception, is that resulting in unnecessary promise fulfillment unwrapping in the non-exception case, where a normal fulfilled value is unwrapped, and then immediately re-wrapped in the outer res promise?
Yup. I don't have any clever ideas for how to avoid this; if you do, let me know!
@getify This analogy comes to mind:
async function foo() {
try {
return doSomething(1000);
} catch (error) {
// This will not run
console.log('CAUGHT!', error);
}
}
versus this:
async function foo() {
try {
return await doSomething(1000);
} catch (error) {
// This will run!
console.log('CAUGHT!!!', error);
}
}
In alignment with this, I think it would make sense for the await to be declared explicitly.
I think I understand now, but I was initially wondering about things like this:
let x = async do { 42; };
typeof x; // ??
IOW, is x here a promise or the value 42? I believe it's a promise, which points to thinking of the "async do" like an "async iife".
And in this particular example, it's effectively the same thing as let x = Promise.resolve(42), right?
FWIW, in my libs where I deal with a lot of promises, I do this sort of thing quite frequently:
let x = isPromise(whatever) ? await whatever : whatever;
The reason is performance, not wanting to "pay" the microtask cost in all these steps. It's sort of a short-circuited path when you have something that's already 42 and not Promise<42>.
I was thinking it might have been nice to have something like that here, where async do conditionally lifts to a promise. Maybe that's not desired, but that's where my probing was sorta headed.
In any case, I think I get it now: think of it like an async function that's immediately executed.
IOW, is
xhere a promise or the value42? I believe it's a promise, which points to thinking of the "async do" like an "async iife". And in this particular example, it's effectively the same thing aslet x = Promise.resolve(42), right?
Right.
FWIW, in my libs where I deal with a lot of promises, I do this sort of thing quite frequently:
let x = isPromise(whatever) ? await whatever : whatever;
If this is common, I think it deserves a new proposal. There could a optional await, which would await if the value is a promise, or jsut return the value if it isn't
let x = await? whatever
FWIW, in my libs where I deal with a lot of promises, I do this sort of thing quite frequently:
let x = isPromise(whatever) ? await whatever : whatever;If this is common, I think it deserves a new proposal. There could a optional await, which would await if the value is a promise, or jsut return the value if it isn't
let x = await? whatever
Doesn't JS normally work this way? You can await non-promises and it just returns that value:
const x = await 42;
x // 42
You can await non-promises and it just returns that value:
Yes, but by spec (AFAIK) it still must spend a microtick doing so, it's not literally synchronous.
You can await non-promises and it just returns that value:
Yes, but by spec (AFAIK) it still must spend a microtick doing so, it's not literally synchronous.
Hm, Is that single-microtick optimisation worth a new language feature? Seems like it wouldn't matter in the grand scheme of things.
it absolutely matters... that's why I go to all the trouble to manually handle this in lots of places where I do an await call, especially in my libraries which can be used by thousands of other devs.
it's not just a performance thing, it's that sometimes you do not want to yield to the microtask queue and potentially experience a hidden race condition where some other microtask can change something that you weren't expecting.
it absolutely matters... that's why I go to all the trouble to manually handle this in lots of places where I do an
awaitcall, especially in my libraries which can be used by thousands of other devs.it's not just a performance thing, it's that sometimes you do not want to yield to the microtask queue and potentially experience a hidden race condition where some other microtask can change something that you weren't expecting.
Sorry, I can't really think of a good example where the latter race condition would occur? That sounds like something that would need to be designed against elsewhere. Maybe there's a common pattern here that I just don't see regularly.
Sorry if this comes across as rude! But I feel like I'm missing something obvious here - as a sub-proposal for await? seems very strange.
so for a do-expression as follows:
let result = do {
something()
// ...
true
}
would the equivalent async-do-expression then be as follows:
let result = await async do {
await something()
// ...
true
}
or (more verbose and thus more probable):
let result = await (async do {
await something()
// ...
true
})