For-await loop inside Promise

138 Views Asked by At

Imagine we have an async generator function:

async f * (connection) {
    while (true) {
        ...
        await doStuff()
        yield value
    }
}

Suppose that this function is virtually endless and gives us results of some async actions. We want to iterate these results:

for await (const result of f(connection)) {
    ...
}

Now imagine we want to break out of this for-await loop when some timeout ends and clean things up:

async outerFunc() {
    setTimeout(() => connection.destroy(), TIMEOUT_MS)

    for await (const result of f(connection)) {
        ...
        if (something) {
            return 'end naturally'
        }
    }
}

Assume that connection.destroy() ends the execution of f and ends the for-await loop. Now it would be great to return some value from the outerFunc when we end by timeout. The first thought is wrapping in a Promise:

async outerFunc() {
    return await new Promise((resolve, reject) => {
        setTimeout(() => {
            connection.destroy()
            resolve('end by timeout')
        }, TIMEOUT_MS)

        for await (const result of f(connection)) { // nope
            ...
            if (something) {
                resolve('end naturally')
            }
        }
    })
}

But we cannot use awaits inside Promise and we cannot make the function async due to this antipattern

The question is: how do we return by timeout the right way?

2

There are 2 best solutions below

0
On

It gets much easier, if you use an existing library that can handle asynchronous generators and timeouts automatically. The example below is using library iter-ops for that:

import {pipe, timeout} from 'iter-ops';

// test async endless generator:
async function* gen() {
    let count = 0;
    while (true) {
        yield count++; // endless increment generator
    }
}

const i = pipe(
    gen(), // your generator
    timeout(5, () => {
        // 5ms has timed out, do disconnect or whatever
    })
); //=> AsyncIterable<number>

// test:
(async function () {
    for await(const a of i) {
        console.log(a); // display result
    }
})();
0
On

Assume that connection.destroy() ends the execution of f and ends the for-await loop.

In that case, just place your return statement so that it is executed when the loop ends:

async outerFunc() {
    setTimeout(() => {
        connection.destroy()
    }, TIMEOUT_MS)

    for await (const result of f(connection)) {
        ...
        if (something) {
            return 'end naturally'
        }
    }
    return 'end by timeout'
}