Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using for await...of with synchronous iterables

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)
like image 812
Ben Aston Avatar asked Mar 16 '20 12:03

Ben Aston


People also ask

Can you use await in a synchronous function?

Async/await helps you write synchronous-looking JavaScript code that works asynchronously. Await is in an async function to ensure that all promises that are returned in the function are synchronized. With async/await, there's no use of callbacks.

Can you await in a for loop?

When you use await , you expect JavaScript to pause execution until the awaited promise gets resolved. This means await s in a for-loop should get executed in series. The result is what you'd expect. This behaviour works with most loops (like while and for-of loops)…

Are for loops asynchronous?

For loops. Combining async with a for (or a for...of ) loop is possibly the most straightforward option when performing asynchronous operations over array elements. Using await inside a for loop will cause the code to stop and wait for the asynchronous operation to complete before continuing.

Does async await make it synchronous?

Or we can say await is only used with an async function. The await keyword is used in an async function to ensure that all promises returned in the async function are synchronized, ie. they wait for each other. Await eliminates the use of callbacks in .

Does await () work with async iterators that are not async?

Note: for await...of doesn't work with async iterators that are not async iterables. On each iteration a value of a different property is assigned to variable. variable may be declared with const, let, or var .

How does for-await-of work for iterables over promises?

Each iterated value is converted to a Promise (or left unchanged if it already is a Promise) via Promise.resolve (). That is, for-await-of works for iterables over Promises and over normal values. The conversion looks like this: Therefore, the following two statements are roughly similar.

Is it possible to use for-await-of with async generators?

You can, but for-await-of works with both synchronous iterables and asynchronous iterables. So, with a function that uses for-await-of, you can use, e.g., an async generator that yields values or a sync generator that yields Promises.

What is the use of await () method in Python?

It invokes a custom iteration hook with statements to be executed for the value of each distinct property of the object. This statement can only be used inside an async function. Note: for await...of doesn't work with async iterators that are not async iterables.


1 Answers

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.

like image 110
Bergi Avatar answered Oct 19 '22 15:10

Bergi