Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Waiting for more than one concurrent await operation

How can I change the following code so that both async operations are triggered and given an opportunity to run concurrently?

const value1 = await getValue1Async(); const value2 = await getValue2Async(); // use both values 

Do I need to do something like this?

const p1 = getValue1Async(); const p2 = getValue2Async(); const value1 = await p1; const value2 = await p2; // use both values 
like image 944
Ben Aston Avatar asked Oct 23 '17 12:10

Ben Aston


People also ask

How do you handle multiple await?

In order to run multiple async/await calls in parallel, all we need to do is add the calls to an array, and then pass that array as an argument to Promise. all() . Promise. all() will wait for all the provided async calls to be resolved before it carries on(see Conclusion for caveat).

Can we use multiple await?

As far as I understand, in ES7/ES2016 putting multiple await 's in code will work similar to chaining . then() with promises, meaning that they will execute one after the other rather than in parallel. So, for example, we have this code: await someCall(); await anotherCall();

Can async function have more than one await?

Using one try/catch block containing multiple await operations is fine. The await operator stores its parent async functions' execution context and returns to the event loop. Execution of the await operator resumes when it is called back with the settled state and value of its operand.

What code pattern do you use when you want to wait for multiple promises to resolve before returning from a function?

The keyword await is used to wait for a Promise. It can only be used inside an async function. This keyword makes JavaScript wait until that promise settles and returns its result.


1 Answers

TL;DR

Don't use the pattern in the question where you get the promises, and then separately wait on them; instead, use Promise.all (at least for now):

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]); 

While your solution does run the two operations in parallel, it doesn't handle rejection properly if both promises reject.

Details:

Your solution runs them in parallel, but always waits for the first to finish before waiting for the second. If you just want to start them, run them in parallel, and get both results, it's just fine. (No, it isn't, keep reading...) Note that if the first takes (say) five seconds to complete and the second fails in one second, your code will wait the full five seconds before then failing.

Sadly, there isn't currently await syntax to do a parallel wait, so you have the awkwardness you listed, or Promise.all. (There's been discussion of await.all or similar, though; maybe someday.)

The Promise.all version is:

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]); 

...which is more concise, and also doesn't wait for the first operation to complete if the second fails quickly (e.g., in my five seconds / one second example above, the above will reject in one second rather than waiting five). Also note that with your original code, if the second promise rejects before the first promise resolves, you may well get a "unhandled rejection" error in the console (you do currently with Chrome v61; update: more recent versions have more interesting behavior), although that error is arguably spurious (because you do, eventually, handle the rejection, in that this code is clearly in an async function¹ and so that function will hook rejection and make its promise reject with it) (update: again, changed). But if both promises reject, you'll get a genuine unhandled rejection error because the flow of control never reaches const value2 = await p2; and thus the p2 rejection is never handled.

Unhandled rejections are a Bad Thing™ (so much so that soon, Node.js will abort the process on truly unhandled rejections, just like unhandled exceptions — because that's what they are), so best to avoid the "get the promise then await it" pattern in your question.

Here's an example of the difference in timing in the failure case (using 500ms and 100ms rather than 5 seconds and 1 second), and possibly also the arguably-spurious unhandled rejection error (open the real browser console to see it):

const getValue1Async = () => {    return new Promise(resolve => {      setTimeout(resolve, 500, "value1");    });  };  const getValue2Async = () => {    return new Promise((resolve, reject) => {      setTimeout(reject, 100, "error");    });  };    // This waits the full 500ms before failing, because it waits  // on p1, then on p2  (async () => {    try {      console.time("separate");      const p1 = getValue1Async();      const p2 = getValue2Async();      const value1 = await p1;      const value2 = await p2;    } catch (e) {      console.error(e);    }    console.timeEnd("separate");  })();    // This fails after just 100ms, because it doesn't wait for p1  // to finish first, it rejects as soon as p2 rejects  setTimeout(async () => {    try {      console.time("Promise.all");      const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);    } catch (e) {      console.timeEnd("Promise.all", e);    }  }, 1000);
Open the real browser console to see the unhandled rejection error.

And here we reject both p1 and p2, resulting in a non-spurious unhandled rejection error on p2:

const getValue1Async = () => {    return new Promise((resolve, reject) => {      setTimeout(reject, 500, "error1");    });  };  const getValue2Async = () => {    return new Promise((resolve, reject) => {      setTimeout(reject, 100, "error2");    });  };    // This waits the full 500ms before failing, because it waits  // on p1, then on p2  (async () => {    try {      console.time("separate");      const p1 = getValue1Async();      const p2 = getValue2Async();      const value1 = await p1;      const value2 = await p2;    } catch (e) {      console.error(e);    }    console.timeEnd("separate");  })();    // This fails after just 100ms, because it doesn't wait for p1  // to finish first, it rejects as soon as p2 rejects  setTimeout(async () => {    try {      console.time("Promise.all");      const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);    } catch (e) {      console.timeEnd("Promise.all", e);    }  }, 1000);
Open the real browser console to see the unhandled rejection error.

In a comment you've asked:

Side question: will the following force waiting for both (and discarding the results) await p1 && await p2?

This has the same issues around promise rejection as your original code: It will wait until p1 resolves even if p2 rejects earlier; it may generate an arguably-spurious (update: or temporary) unhandled rejection error if p2 rejects before p1 resolves; and it generates a genuine unhandled rejection error if both p1 and p2 reject (because p2's rejection is never handled).

Here's the case where p1 resolves and p2 rejects:

const getValue1Async = () => {    return new Promise(resolve => {      setTimeout(resolve, 500, false);    });  };  const getValue2Async = () => {    return new Promise((resolve, reject) => {      setTimeout(reject, 100, "error");    });  };    (async () => {    try {      const p1 = getValue1Async();      const p2 = getValue2Async();      console.log("waiting");      await p1 && await p2;    } catch (e) {      console.error(e);    }    console.log("done waiting");  })();
Look in the real console (for the unhandled rejection error).

...and where both reject:

const getValue1Async = () => {    return new Promise((resolve, reject) => {      setTimeout(reject, 500, "error1");    });  };  const getValue2Async = () => {    return new Promise((resolve, reject) => {      setTimeout(reject, 100, "error2");    });  };    (async () => {    try {      const p1 = getValue1Async();      const p2 = getValue2Async();      console.log("waiting");      await p1 && await p2;    } catch (e) {      console.error(e);    }    console.log("done waiting");  })();
Look in the real console (for the unhandled rejection error).

¹ "...this code is clearly in an async function..." That was true in 2017 when this question and answer were written. Since then, top-level await happened/is happening.

like image 180
T.J. Crowder Avatar answered Oct 02 '22 07:10

T.J. Crowder