How does the order of concurrent promises work in an asynchronous function?

Viewed 69

I noticed an example of concurrent promises from MDN:

async function foo() {
   const p1 = new Promise((resolve) => setTimeout(() => resolve('1'), 1000))
   const p2 = new Promise((_,reject) => setTimeout(() => reject('2'), 500))
   const results = [await p1, await p2];
}
foo().catch(() => {}) // Attempt to swallow all errors...

MDN states:

in the following code an unhandled promise rejection error will be thrown, even if a .catch handler has been configured further along the promise chain. This is because p2 will not be "wired into" the promise chain until control returns from p1.

What exactly does this mean? If await waits for p1's promise to resolve first, why then does the rejection error occur for p2? I am very new to asynchronous function and am grateful for any new insights!

2 Answers

The problem is that the interpreter isn't waiting for p2's promise to resolve while it's still waiting for p1. Before it runs across the line await p2, the p2 is essentially dangling - it's not connected to anything else until it gets awaited. So, if p2 throws before it's connected to a .catch - either via a .catch in the function itself, or via an await, connecting it to an outside .catch - you'll get an unhandled rejection.

Another way of illustrating the same problem, using only a single Promise:

function foo() {
   const p2 = new Promise((_,reject) => setTimeout(() => reject('2'), 500))
   setTimeout(() => {
     p2.catch(() => {});
   }, 1000);
}
foo();

In your snippet's example, p1 is only important in that it causes a delay before the await p2 expression is encountered. In my example above, the setTimeout is doing something similar - it's attaching a .catch handler after the promise has rejected, resulting in an unhandled rejection.

Adding some console.log statements to your program will help you see order of operations

async function foo() {
   console.log("foo")
   const p1 = new Promise((resolve) => {
     console.log("p1 promise")
     setTimeout(_ => {
       console.log("p1 resolve")
       resolve('1')
     }, 1000)
   })
   const p2 = new Promise((_,reject) => {
     console.log("p2 promise")
     setTimeout(_ => {
       console.log("p2 reject")
       reject('2')
     }, 500)
   })
   console.log("results") 
   return [
     (console.log("await p1"), await p1),     
     (console.log("await p2"), await p2)
   ]
   
}
foo().then(console.log, console.error)

output

order console.log
1 foo
2 p1 promise
3 p2 promise
4 results
5 await p1
6 p2 reject
7 p1 resolve
8 await p2
9 Error: 2

explanation

  1. So when we run foo, it's expected that we see message 1, "foo", first, as that is the first line of the program.

  2. Next p1 is created and we see message 2, "p1 promise". This shows you that the body of new Promise(... => body) is run immediately.

  3. Next p2 is created and we see message 3, "p2 promise", following the same explanation as above.

  4. Next we see message 4, "results". If you expected to see the messages inside the setTimeout, those are delayed in the event queue and we will see them later. This is the express purpose of setTimeout

  5. Next is message 5, "await p1", and we are waiting for p1 to resolve.

  6. While we are waiting for p1 to resolve, p2 rejects and we see message 6, "p2 reject".

  7. p1 finally resolves and we see message 7, "p1 resolve".

  8. Next we move onto the final value in the result array and we wee message 8, "await p2". p2 has already been fulfilled at this point, and it has resulted in a rejected error. At this point foo immediately exits and the call to foo() results in a rejected promise. If there was code after this point, it would not be run. This is the same as throwing an error in synchronous code.

  9. Finally we see message 9, "Error: 2", which is the result of console.error catching the rejected promise returned by foo()

Related