16

MDN says for await...of has two use-cases:

The for await...of statement creates a loop iterating over async iterable objects as well as on sync iterables,...

I was previously aware of the former: async iterables using Symbol.asyncIterator. But I am now interested in the latter: synchronous iterables.

The following code iterates over a synchronous iterable - an array of promises. It appears to block progess on the fulfilment of each promise.

async function asyncFunction() {
    try {
        const happy = new Promise((resolve)=>setTimeout(()=>resolve('happy'), 1000))
        const sad = new Promise((_,reject)=>setTimeout(()=>reject('sad')))
        const promises = [happy, sad]
        for await(const item of promises) {
            console.log(item)
        }
    } catch (err) {
        console.log(`an error occurred:`, err)
    }
}

asyncFunction() // "happy, an error occurred: sad" (printed in quick succession, after about 5 seconds)

The behavior appears to be akin to awaiting each promise in-turn, per the logic shown below. Is this assertion correct?

async function asyncFunction() {
    try {
        const happy = new Promise((resolve)=>setTimeout(()=>resolve('happy'), 1000))
        const sad = new Promise((_,reject)=>setTimeout(()=>reject('sad')))
        const promises = [happy, sad]
        for(let p of promises) {
            const item = await p
            console.log(item)
        }
    } catch (err) {
        console.log(`an error occurred:`, err)
    }
}

asyncFunction() // "happy, an error occurred: sad" (printed in quick succession, after about 5 seconds)

I ask because this pattern of code has an implicit rejection wire-up pitfall that Promise.all and Promise.allSettled avoid, and it seems strange to me that this pattern would be explicitly supported by the language.

window.addEventListener('unhandledrejection', () => {
  console.log('unhandled rejection; `sad` was not being awaited at the time it rejected')
})

async function asyncFunction() {
    try {
        const happy = new Promise((resolve)=>setTimeout(()=>resolve('success'), 1000))
        const sad = new Promise((_,reject)=>setTimeout(()=>reject('failure')))
        const promises = [happy, sad]
        for(let p of promises) {
            const item = await p
            console.log(item)
        }
    } catch (err) {
        console.log(`an error occurred:`, err)
    }
}

asyncFunction() // "unhandled rejection; `sad` was not being awaited at the time it rejected" (after about zero seconds), and then "happy, an error occurred: sad" (printed in quick succession, after about 5 seconds)
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • 2
    What exactly is your question? It seems like the examples you provided work – Sagi Rika Mar 16 '20 at 13:16
  • Is my description of `for await... of` with synchronous iterables, correct, and if so, does it matter that that pattern can emit unhandled rejection errors? – Ben Aston Mar 16 '20 at 13:20
  • "Is it correct" isn't a question. "Correct" is whatever you say it is. – Robert Harvey Mar 16 '20 at 13:20
  • Can you demonstrate via code the emitting of unhandled rejection errors that you described? – Robert Harvey Mar 16 '20 at 13:21
  • The final code demonstrates it. Correct has a well-defined meaning in this context because I have provided the code to describe what I think it is doing. If the behavior matches my code, then my code is correct, otherwise my understanding is incorrect. Also the observation _"Correct" is whatever you say it is._ is clearly untrue. Correct has a well defined meaning in this context. – Ben Aston Mar 16 '20 at 13:21

2 Answers2

7

Yes, it is strange, and you should not do this. Don't iterate arrays of promises, it leads exactly to the unhandled-rejections problem you mentioned. (See also this more specific explanation.)

So why is this supported in the language? To continue with the sloppy promise semantics.

You can find the exact reasoning in this comment of the issue discussing this part of the proposal:

I think we should fall back to Symbol.iterator because our current Promise semantics are all about allowing sync things to be used as async things. You might call this "sloppiness". It follows @groundwater's logic above, but I just want to spell out the parallels in more detail.

The "chaining" semantics of .then are all about this. You can return a Promise from .then or a scalar value; it's all the same. You call Promise.resolve not to wrap something in a Promise, but to cast something to a Promise--get an asynchronous value when you have something-or-other.

The semantics of async and await are all about being sloppy as well. You can slap await on any non-Promise expression in an async function and everything works fine, exactly the same way, except that you yield control to the job queue. Similarly, you can "defensively" put async around whatever you want, as long as you await the result. If you have a function that returns a Promise--whatever! you can make that an async function, and, from a user perspective, nothing changes (even if, technically, you get a different Promise object out).

Async iterators and generators should work the same way. Just like you can await a value that, accidentally, wasn't a Promise, a reasonable user would expect to be able to yield* a sync iterator within an async generator. for await loops should similarly "just work" if a user defensively marks a loop that way, thinking that they maybe might be getting an async iterator.

I think it would be a big deal to break all of these parallels. It would make async iterators less ergonomic. Let's discuss this the next time async generators/iterators come up on the agenda at TC39.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thank you. Is an event emitted, or is it actually some other kind of error? I ask because I thought events were part of the WebAPI. Is emission of events used in a similar fashion, in other parts of the spec? – Ben Aston Mar 16 '20 at 13:49
  • @52d6c6af Are you referring to the `unhandledrejection` events? – Bergi Mar 16 '20 at 13:52
  • Yes. It's just that to intercept the "error" I used `window.addEventListener('unhandledrejection',...` In short: it is the only instance I can bring to mind, of this kind of error emission by JavaScript. I'm almost certainly wrong to think this, however. Finally: does the emission of this "error" ever really matter beyond having an unwanted error message in the console? – Ben Aston Mar 16 '20 at 13:58
  • 1
    @52d6c6af See [here](https://stackoverflow.com/a/58922592/1048572) and [there](https://stackoverflow.com/a/57261820/1048572) for how this is specified in a joint effort between ECMAScript and Web API specs. No, the event doesn't really matter, it's too late already when you got this. Afaics, it is only used for monitoring client-side errors. – Bergi Mar 16 '20 at 14:36
  • If it doesn't really matter, is the advice still "do not iterate arrays of promises", or is it rather "be aware that this does not exhibit fail-fast behavior under some circumstances"? – Ben Aston Mar 16 '20 at 14:51
  • @52d6c6af I meant that handling the event doesn't really matter, as there's not much you can do about the problem in the event handler. I still consider it an error, and a programmer mistake to write such code. In node.js, it could crash your application just like an unhandled exception. – Bergi Mar 16 '20 at 16:00
  • But wrapping the `for await...of` in a `try...catch` still catches the error, albeit after the promises preceding the rejected promise, have resolved. My point is that the error can still be handled - just at a different time. – Ben Aston Mar 16 '20 at 16:04
  • Not after your application already has crashed from the unhandled rejection. Just don't do it. – Bergi Mar 16 '20 at 18:00
  • I think this is a problem with the language design here because a lot of devs will guess that `for await...of` is for enumerating promises, and even worse - it kind-of works! – Ben Aston Mar 16 '20 at 18:02
  • @Bergi I get a feeling you don't think `for await... of` has much of use. Am I right ? I am struggling with a practical use of it. – Qiulang Apr 18 '21 at 14:52
  • @Qiulang `for await … of` is useful where the sequence itself is generated asynchronously (and where its length is not known beforehand to the consumer). A perfect example is paginated fetching of a list from an api. Less perfect would be reading files from a large directory - while one could write that code in two steps (getting an array of filenames, then iterating that), sometimes you prefer an api surface that has them packed in one: an asynchronous iterator where you always get filename and contents together. – Bergi Apr 18 '21 at 15:03
  • @Bergi I asked a question https://stackoverflow.com/questions/67103606/some-practical-example-of-asynchronous-iterationfor-await-of-in-action. In my question I did mention paginated fetching is one example of using `for await of`. Maybe you can just answer my question ? – Qiulang Apr 18 '21 at 15:07
0

The sad promise isn't being awaited when it fails - that code needs to finish waiting on happy before it can begin to wait on sad. The sad promise is failing before happy resolves. (Promise.all is a tool better suited to this use-case)

Gershom Maes
  • 7,358
  • 2
  • 35
  • 55
  • 1
    I know. Hence my question. If `Promise.all` is a better solution, why does the language cater for this syntax? `for await...of` could easily have been implemented to simply enumerate asynchronous iterables. But they catered for it to enumerate synchronous iterables (but with a (seeming?) pitfall). Why? – Ben Aston Mar 16 '20 at 13:25
  • 1
    Ah, I misunderstood. Are we asking why `for await ... of` accepts synchronous iterables? I'd imagine to support async generators which conditionally may return synchronous items. – Gershom Maes Mar 16 '20 at 13:29
  • Yes, especially so given that it appears to introduce a rejection wire-up pitfall. – Ben Aston Mar 16 '20 at 13:33
  • In my opinion the pitfall is more generally in creating a promise, and not awaiting it immediately. Unfortunately this pitfall is also often a very useful feature. – Gershom Maes Mar 16 '20 at 15:13