Catch Promise rejection at a later time [duplicate

2019-06-15 06:49发布

问题:

This question already has an answer here:

  • Waiting for more than one concurrent await operation 4 answers

How do I retrieve the result of a promise at a later time? In a test, I am retrieving an email before sending further requests:

const email = await get_email();
assert.equal(email.subject, 'foobar');
await send_request1();
await send_request2();

How can I send the requests while the slow email retrieval is going on?

At first, I considered awaiting the email later:

// This code is wrong - do not copy!
const email_promise = get_email();
await send_request1();
await send_request2();
const email = await email_promise;
assert.equal(email.subject, 'foobar');

This works if get_email() is successful, but fails if get_email() fails before the corresponding await, with a completely justified UnhandledPromiseRejectionWarning.

Of course, I could use Promise.all, like this:

await Promise.all([
    async () => {
        const email = await get_email();
        assert.equal(email.subject, 'foobar');
    },
    async () => {
        await send_request1();
        await send_request2();
    },
]);

However, it makes the code much harder to read (it looks more like callback-based programming), especially if later requests actually depend on the email, or there is some nesting going on. Is it possible to store the result/exception of a promise and await it at a later time?

If need be, here is a testcase with mocks that sometimes fail and sometimes work, with random timings. It must never output UnhandledPromiseRejectionWarning.

回答1:

const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const send_request1 = () => wait(300), send_request2 = () => wait(200);
async function get_email() {
    await wait(Math.random() * 1000);
    if (Math.random() > 0.5) throw new Error('failure');
    return {subject: 'foobar'};
}

const assert = require('assert');
async function main() {
    // catch possible error
    const email_promise = get_email().catch(e => e);
    await send_request1();
    await send_request2();
    // wait for result
    const email = await email_promise;
    // rethrow eventual error or do whatever you want with it
    if(email instanceof Error) {
      throw email;
    }
    assert.equal(email.subject, 'foobar');
};

(async () => {
    try {
        await main();
    } catch(e) {
        console.log('main error: ' + e.stack);
    }
})();


回答2:

In case it's guaranteed that promise rejection will be handled later, a promise can be chained with dummy catch to suppress the detection of unhandled rejection:

try {
    const email_promise = get_email();
    email_promise.catch(() => {}); // a hack
    await send_request1();
    await send_request2();
    const email = await email_promise;
    assert.equal(email.subject, 'foobar');
} catch (err) {...}

The problem with this approach is that there are two concurrent routines but the code doesn't express this, this is a workaround for what is usually done with Promise.all. The only reason why this workaround is feasible is that there are only 2 routines, and one of them (get_email) requires to be chained with then/await only once, so a part of it (assert) can be postponed. The problem would be more obvious if there were 3 or more routines, or routines involved multiple then/await.

In case Promise.all introduces unwanted level of lambda nesting, this can be avoided by writing routines as named functions, even if they aren't reused anywhere else:

async function assertEmail() {
    const email = await get_email();
    assert.equal(email.subject, 'foobar');
}

async function sendRequests() {
    await send_request1();
    await send_request2();
}

...

try {
    await Promise.all([assertEmail(), sendRequests()]);
} catch (err) {...}

This results in clean control flow and verbose but more intelligible and testable code.



回答3:

So, I want to explain why we behave this way in Node.js:

// Your "incorrect code" from before
const email_promise = get_email(); // we acquire the promise here
await send_request1(); // if this throws - we're left with a mess
await send_request2(); // if this throws - we're left with a mess
const email = await email_promise;
assert.equal(email.subject, 'foobar');

That is, the reason we behave this way is to not deal with the "multiple rejections and possibly no cleanup" scenario. I'm not sure how you ended up with the long code for Promise.all but this:

await Promise.all([
    async () => {
        const email = await get_email();
        assert.equal(email.subject, 'foobar');
    },
    async () => {
        await send_request1();
        await send_request2();
    },
]);

Can actually be this:

let [email, requestData] = await Promise.all([
  get_email(),
  send_request1().then(send_request2)
]);
// do whatever with email here

It's probably what I would do.