Awaiting within an arrow function #7

Closed
jakearchibald opened this Issue Mar 27, 2014 · 20 comments

Projects

None yet

9 participants

@jakearchibald

Assuming getJSON returns a promise for some JSON, does this work:

async function renderChapters(urls) {
  urls.map(getJSON).forEach(j => addToPage((await j).html));
}

Does the await within the arrow function halt the execution of both the forEach and renderChapters, or does it error because the arrow function is not an async function?

If not, should we have a way to await to the parent async function?

@getify
getify commented Mar 27, 2014

[Edit: I completely didn't realize that exact thing I'm mentioning here is already mentioned in the main README of this spec.... duh...

https://github.com/lukehoban/ecmascript-asyncawait#await-and-parallelism
]

Probably isn't possible, but IMO it'd be awesome if yield* and await* (there is one, right?) could do this:

async function renderChapters(urls) {
  await* urls.map(getJSON).forEach(j => addToPage((await j).html));
}

Where the await* basically "spreads out" across the iteration callbacks.

I believe there was a thread about await* being used on an array of promises, dunno where that ended up, but this is sorta an extension of that idea, where it delegates into the array iteration callbacks.

Just green-field dreaming.

@domenic
Member
domenic commented Mar 27, 2014

See @dherman's "Why Coroutines Won't Work For the Web". Note that this is equivalent to what you're doing here: calling a function (namely forEach) without awaiting it, and expecting that function to somehow suspend your execution. It's probably clearer written like this:

function doStuff(j) {
  addToPage((await j).html);
}

async function renderChapters(urls) {
  urls.map(getJSON).forEach(doStuff); // (1)
  console.log("hi"); // (2)
}

Here you're expecting the first line (1) to "block", preventing line (2) from executing until all the js are awaited. This is bad news bears.


A version that could work would be something that awaits all async operations, e.g.

async function renderChapters(urls) {
  await asyncForEach(urls.map(getJSON), async (j) => addToPage((await j).html)));
}

using the async arrow function syntax from this repo.

@jakearchibald

I understand why the doStuff example doesn't work, whereas I thought arrow functions may be different. Same as this is scoped to the parent function, I thought await would be too.

@getify
getify commented Mar 27, 2014

[Edit: later in this thread, thanks to @domenic, I realized why it wasn't "blocking" that I was assuming, but the implicit wrapping in Promise.all. So, disregard all the rest of this comment]

In my mind (can't speak for @lukehoban's mention in the main README, but I suspect true as well), await* would sorta be doing both an await (to block current async function execution) and a delegated await* to all the iterated function callbacks.

So it's purposely, before returning:

  1. implicitly casting all the function callbacks into async functions (or maybe you have to explicitly make them async function expressions?)
  2. waiting for all those async delegated function callbacks from the iteration, serially, in order

...before returning, sorta like (as @lukehoban mentioned) a Promise.all but with the blocking semantic.

@domenic
Member
domenic commented Mar 27, 2014

I thought arrow functions may be different. Same as this is scoped to the parent function, I thought await would be too.

await is a keyword, not an object; objects traditionally are captured by scopes, with the this exception being a weird one. await controlling the parent inside arrows would be like return controlling a parent inside arrows.

What would you think about

let doStuff = j => addToPage((await j).html);

async function renderChapters(urls) {
  urls.map(getJSON).forEach(doStuff); // (1)
  console.log("hi"); // (2)
}

? It has the same problem but now we're using arrow functions.

@domenic
Member
domenic commented Mar 27, 2014

await* x is just await Promise.all(x), @getify. It does not add deep coroutines in any way shape or form.

Remember that await does not await function calls; it awaits promises.

@jakearchibald

Tbh, I wouldn't mind return behaving in the same way.

I imagine your example would syntax error because await was used outside an async function. However, if it were inside an async function (so there were 2 nested async functions), you're right, I don't know what it would do.

Case closed, I see why it's not possible. It's a shame it isn't, though.

@getify
getify commented Mar 27, 2014

@jakearchibald

whereas I thought arrow functions may be different. Same as this is scoped to the parent function, I thought await would be too.

I actually think the real problem with this assumption (or desire) is that you're calling await inside a normal (or in this case, arrow) function, whereas I believe the spirit of the async/await proposal is that you can only use await inside an async function.

So, if you had done:

async function renderChapters(urls) {
  urls.map(getJSON).forEach(async function(j) { addToPage((await j).html) });
}

Then it's pretty clear that await j is applying only to its parent async function(j), not to the outer parent renderChapters(..).

@domenic
Member
domenic commented Mar 27, 2014
async function renderChapters(urls) {
  urls.map(getJSON).forEach(async function(j) { addToPage((await j).html) });
  console.log("hi"); // (inserted)
}

right, but forEach doesn't do anything with its callback's return types, so the fact that async function (j) { ... } returns a promise doesn't do anything; the (inserted) line will be executed immediately, since forEach doesn't await.

That's why I used asyncForEach in my above example.

@domenic
Member
domenic commented Mar 27, 2014

Tbh, I wouldn't mind return behaving in the same way.

What would you expect this to do?

function foo(x) {
  setTimeout(() => return x + 1, 1000);
}

foo(1); // you would expect a return value of 2, somehow??
@jakearchibald

Yeah, as I said at the end of the comment, I realise now why it's not possible.

@lukehoban lukehoban closed this Apr 8, 2014
@Ajedi32
Ajedi32 commented Oct 16, 2015

So just to clarify, the original example (which doesn't work):

async function renderChapters(urls) {
  urls.map(getJSON).forEach(j => addToPage((await j).html));
}

Could actually be written in one of three ways, with slightly differing results. If you write it like this:

async function renderChapters(urls) {
  for (let j of urls.map(getJSON)) {
    addToPage((await j).html);
  }
}

Which is the closest to what was originally written, then all getJSON operations will happen in parallel, with the results being added to the page sequentially as they come in. The result of later requests will not be added to the page before the result of previous requests. (E.g. The result of the second request will not be added to the page before the first one.)

If you write it like this:

async function renderChapters(urls) {
  (await Promise.all(urls.map(getJSON))).forEach(j => addToPage(j.html));
}

Then all getJSON operations will happen in parallel, and the application will wait until all requests are complete before adding them all to the page at once.

Or if you write it like this:

async function renderChapters(urls) {
  await Promise.all(urls.map(getJSON).map(async j => addToPage((await j).html)));
}

Then all getJSON operations will happen in parallel, and each object will be added to the page as it arrives (possibly out of order).

Example here: http://www.es6fiddle.net/iftvukev/ (Or on Babel.)

Edit: Replaced await* with await Promise.all(...)

Edit 2: Added Babel mirror of example, clarify that original example is broken.

@domenic
Member
domenic commented Oct 16, 2015

await* is nonstandard syntax

@Ajedi32
Ajedi32 commented Oct 16, 2015

It is? Eh, well you can always just replace it with await Promise.all(...) since that's really all it's doing anyway. await* did work fine for me in Babel though.

@domenic
Member
domenic commented Oct 16, 2015

Yes, Babel is not standards compliant.

@kittens
kittens commented Oct 16, 2015

Actually @domenic, Babel is spec complaint. The addition of something non standard doesn't make it not compliant, not unless it's explicitly forbidden. That doesn't matter anyway since it's already been removed for Babel 6 (babel/babel#2466)

On Fri, Oct 16, 2015 at 4:40 PM, Domenic Denicola
notifications@github.com wrote:

Yes, Babel is not standards compliant.

Reply to this email directly or view it on GitHub:
#7 (comment)

@bterlson
Member

To be pedantic, no implementation is completely standards compliant. @domenic's statement is not wrong but not saying much either :-P

@barneycarroll

Sorry for reviving this thread all these months later. This thread is cited in multiple places as an explanation of why async arrow functions are impossible, but I'm not getting that. AFAICT, the point @domenic is making is that you are unleashing event loop Zalgo if you expect a synchronous operation to be able to invoke code which awaits internally. That's fair enough, the forEach example is legit, and it's a fundamental issue to grok when you want to do await: one can only await directly inside an async scope.

To be clear: forEach synchronously executes the iterator function for each item in the array. We can't expect the parser to go "oh but obs we want some kind of async forEach" based on the contents of the iterator, because A) that's way too much assumptive black magic and B) it's ambiguous: should the iteration happen sequentially or parallel? @Ajedi32 shows us how to get around that with solid, explicit code that doesn't just hand wave fundamental concerns of asynchrony

So that's solved. What I don't get is how arrows relate to any of this. AFAICT the only concern there is that we have lexical this, but isn't that just as problematic as exposing any higher scope state? And even if it's a special kind of dangerous and horrible, isn't that just bad practice or 'considered dangerous' as opposed to logically impossible or semantically ambiguous?

@getify
getify commented Mar 14, 2016

async arrow functions are impossible

They're not. (async x => await x)(y) would be just fine, IIUC.

What I don't get is how arrows relate to any of this.

IMO, they don't. I was incorrect earlier in the thread, for sure, but what I now realize is that @domenic's first comment was totally correct... Any kind of function, normal or arrow or otherwise, couldn't call await from inside it and have that affect its outer async function. That'd be a deep continuation, and it's just not going to happen.

If this thread is related to arrow functions particularly, it's that it highlights how easy it is to mistake an arrow function (conceptually) for just a block, in which you would expect to still be able to easily await. I suspect if the OP had used a normal inner function, arrow's might never have been brought into it.

@Marak
Marak commented Mar 25, 2016

This conversation is mildly terrifying to me.

Let's try not assume we are all are correct and end up in another peerDependencies situation like we had with npm.

Try to stay open-minded. Cheers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment