javascriptpromiseasync-awaitgeneratorasync-iterator

Async Generator: Yielding a rejected promise


I've been playing around with async generators in an attempt to make a "promise ordering" generator which takes an array of promises and yields out promises one by one in the order they resolve or reject. So something like:

async function* orderProms(prom_arr) {

    // Make a copy so the splices don't mess it up.
    const proms = [...prom_arr];

    while (proms.length) {
        // Tag each promise with it's index, so that we can remove it for the next loop.
        const {prom, index} = await Promise.race(proms.map((prom, index) => prom.then(
            () => ({prom, index}),
            () => ({prom, index})
        )));

        proms.splice(index, 1);
        yield prom;
    }
}

With the idea to consume this generator like so:

const resAfter = (val, delay) => new Promise(res => setTimeout(() => res(val), delay));
const rejAfter = (val, delay) => new Promise((_, rej) => setTimeout(() => rej(val), delay));

const promises = [
    resAfter("Third", 3000),
    resAfter("First", 1000),
    rejAfter("Second", 2000), // NOTE: this one rejects!
];

(async () => {

    let ordered = orderProms(promises);

    let done = false;
    for (let next_promise = ordered.next(); !done; next_promise = ordered.next()) {
        const next = await next_promise
            .catch(err => ({done: false, value: `Caught error: ${err}`}));

        done = next.done;
        if (!done) console.log(next.value);
    }
})()

However, I've noticed that this will reach up to the second promise, then the generator will halt. It seems to be because of the rejected "second" promise. Calling yield prom in the generator will create an exception in the generator when the prom is rejected.

But this is the source of my confusion. I do not want to create an exception here, I just want to yield the rejected promise as the value of the iterator result. I don't want it to be unwrapped. It's almost like this is being treated as yield await prom;, but as you can see there is no await call.

What is going on here and how can I simply yield a rejected promise as-is from this generator.


Here's the above code in a runnable snippet:

async function* orderProms(prom_arr) {

    // Make a copy so the splices don't mess it up.
    const proms = [...prom_arr];

    while (proms.length) {
        // Tag each promise with it's index, so that we can remove it for the next loop.
        const {prom, index} = await Promise.race(proms.map((prom, index) => prom.then(
            () => ({prom, index}),
            () => ({prom, index})
        )));

        proms.splice(index, 1);
        yield prom;
    }
}

const resAfter = (val, delay) => new Promise(res => setTimeout(() => res(val), delay));
const rejAfter = (val, delay) => new Promise((_, rej) => setTimeout(() => rej(val), delay));

const promises = [
    resAfter("Third", 3000),
    resAfter("First", 1000),
    rejAfter("Second", 2000), // NOTE: this one rejects!
];

(async () => {

    let ordered = orderProms(promises);

    let done = false;
    for (let next_promise = ordered.next(); !done; next_promise = ordered.next()) {
        const next = await next_promise
            .catch(err => ({done: false, value: `Caught error: ${err}`}));

        done = next.done;
        if (!done) console.log(next.value);
    }
})()


Solution

  • It's almost like this is being treated as yield await prom. What is going on here?

    Exactly that is how async generators behave.

    how can I simply yield a rejected promise as-is from this generator.

    You cannot. Notice that an async iterator is expected to be consumed by

    try {
        for await (const value of orderProms(promises)) {
            console.log(value);
        }
    } catch(err) {
        console.error('Caught error: ', err);
    }
    

    There is no facilitation for individual error handling in the syntax. When there's an exception, the loop stops, the generator is done. Point.

    So what can you do? I see three choices: