12

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.

phihag
  • 278,196
  • 72
  • 453
  • 469
  • I don't understand this. You can catch errors with try/catch when using async/await. And you can use .catch() chained to promise objects. Your posted buffer solution is basically just an implementation of .then() . So as long as your promise is rejected, adding .catch() to it at a later time will give you the error. – Shilly Dec 20 '18 at 13:41
  • @Shilly Great, mind posting your solution then? My answer below contains a complete test (which will sometimes fail, sometimes work, with different timings). When run in node multiple times, this buffer answer always outputs a correct result, and **never** causes an `UnhandledPromiseRejectionWarning`. If all I need is just a couple `await`s or `then`s, all the better! – phihag Dec 20 '18 at 13:46
  • What do you mean? Do you need to catch the individual errors? Is the point that you DO want a UnhandledPromiseRejectionWarning instead of catching it? – Shilly Dec 20 '18 at 13:47
  • No, I do **not** want any `UnhandledPromiseRejectionWarning`s. Any errors should be thrown as usual in a promise. In other words, the example code should always either work or catch the exception (output `main error: Error: failure`). – phihag Dec 20 '18 at 13:49
  • 1
    Is it really necessary that the email is retrieved and the requests are sent in the same method? If possible, it seems like it would be a lot cleaner to dispatch three separate threads. You're effectively trying to solve a concurrency problem here using tools that aren't really up for the job. – Jared Goguen Dec 20 '18 at 14:06
  • If it really is necessary, can you elaborate on why `Promise.all` does not fit your needs? – Jared Goguen Dec 20 '18 at 14:11
  • @JaredGoguen I considered that, but it makes my code much harder to read, since in there are requests down the line that actually do use the email. Using the [helper function in my answer](https://stackoverflow.com/a/53869593/35070), it's all just a series of `awaits`, very similar to the incorrect code block in the question. – phihag Dec 20 '18 at 14:11
  • Sorry, I'm giving up. Stuff like this is why I don't use async/await in complex apps, since it makes keeping track of what's waiting on what more complicated than I like it to be, since you need to retry/catch or reawait or cache the intermediates since they are values instead of promises. I like being able to `get_email.then( render ).catch( logError );` and then down the line just start a new `.then()` chain on the same promise. It's easier to read for me personally than sifting through nested try/catch blocks just to get the correct workflow. – Shilly Dec 20 '18 at 15:18
  • @Shilly Actually I think [@ponury-kostek 's code](https://stackoverflow.com/a/53871079/35070) is much more readable then any handling of Promises without `async-await`. – phihag Dec 20 '18 at 15:31
  • ponury just wrote my second answer inline instead of making it a seperate buffer_promise function. And my second answer wasn't what you wanted either, since it didn't work in your code. So when you isolate the .catch() into a different function again, you'll probably get the same issue I had, that the outer try/catch won't get triggered without rethrowing the error. It's basically what I wrote in my very first comment. But to each their own. Good luck and have fun! – Shilly Dec 20 '18 at 15:50
  • @Shilly I appreciate your effort, but please note that your first comment was wrong: You can *not* just call `.catch` at a later time, because that will cause an unhandled rejection - in the best case a warning, in the worst case a process termination (i.e. the code attaching `.catch` will never be executed). Luckily, in computing there is no need to debate; the machine is the ultimate determinator of truth. When you run [this example with your suggested `.catch` at a later time](https://repl.it/@phihag/DecisiveThirstyInvocation) online, you'll see it. – phihag Dec 21 '18 at 00:22

3 Answers3

6
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);
    }
})();
ponury-kostek
  • 7,824
  • 4
  • 23
  • 31
  • By just changing the top lines to `const email_promise = get_email(); email_promise.catch(e => e)`, all the rethrowing code is no longer necessary, is it? – phihag Dec 24 '18 at 09:33
  • Rethrowing code is necessary, without it will 'resolve' with error, like 'resolve(new Error ())' – ponury-kostek Dec 27 '18 at 12:54
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.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Can you elaborate what the problem is with 3 or more promises? Because that's what I actually have in my code. The problem in my real code is that later requests may depend on the email. Also, it's much more readable as a series of `await` statements, especially if that just allows us to express synchronization points (= all 3 concurrent tasks must be done here) natively, _without_ nesting or helper functions. – phihag Dec 23 '18 at 17:59
  • 1
    *it's much more readable as a series of await statements* - the thing that confuses me here is that you arbitrarily chose one of routines (a sequence of send_request*) to write it in sync-like way, while another routine (get_email) goes sideways. Control flow will be harder to follow if there are more promises. Why should one of these routines be considered first-class and written in series and other go sideways? That's what looks suspicious here, because usually routines like that are treated as equal. – Estus Flask Dec 23 '18 at 19:03
  • `get_email` is a slow operation that needs no further supervision, so yes, it's not nearly as important to the main flow, at least until I actually need its return value. Is that really that unusual for a concurrent application? – phihag Dec 23 '18 at 20:31
1

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.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Well, that's the downside of simplifying code for StackOverflow: My real code is much more complex (it's an integration test). There's a dozen requests being sent, with synchronous code to compute values – it's just real-life code. Expressing the asynchronity of some requests with a simple `async..await` would be *awesome*, and be much ore readable then having a bunch of nested `Promise.all`. – phihag Dec 23 '18 at 17:55