Async Generator: Yielding a rejected promise

◇◆丶佛笑我妖孽 提交于 2021-02-07 21:54:23

问题


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);
    }
})()

回答1:


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:

  • just keep it as is and treat failing early as a feature (similar to Promise.all)
  • handle errors (either in orderProms or before passing promises into it) and yield tuples of promise status and value

    for await (const value of orderProms(promises.map(prom =>
        prom.catch(err => `Caught error: ${err}`)
    ))) {
        console.log(value);
    }
    
  • use a normal (non-async) generator from which you yield one promise after the other manually, to be able to use it in the way you want



回答2:


You could let the promises resolve to something similar as you get from Promise.allSettled:

async function* orderProms(prom_arr) {
    // Make a copy so the splices don't mess it up.
    const proms = new Set(prom_arr.map((prom, index) => ({prom, index})));
    while (proms.size) {
        const settled = await Promise.race(Array.from(proms, obj => obj.prom.then(
            value => Object.assign(obj, { value, status: "fulfilled" }),
            error => Object.assign(obj, { error, status: "rejected" }),
        )));
        proms.delete(settled);
        let { prom, ...rest } = settled;
        yield rest;
    }
}

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 () => {
    for await (let result of orderProms(promises)) {
        console.log(JSON.stringify(result));
    }
})().catch(err => console.log(err.message));


来源:https://stackoverflow.com/questions/62395474/async-generator-yielding-a-rejected-promise

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!