Awaiting multiple promises inside an async function with try catch throws anyways

Viewed 336

I can't understand why the following code doesn't throw:

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        const p1 = Stop(500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)

        await p1
        await p2
    } catch (err) {
        console.log('error catched')
    }
}

main()

But whenever I invert the order of p1 and p2 promises, like this:

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        const p1 = Stop(500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)

        await p2
        await p1
    } catch (err) {
        console.log('error catched')
    }
}

main()

Then an uncaught exception is thrown. I suppose that doing concurrent tasks like that without .catch functions is dangerous, but I thought the async code within a try catch would never throw.

Why isn't this the case, exactly?

3 Answers

First things first, let's take a step back and remove the awaits all together:

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        const p1 = Stop(500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)
    } catch (err) {
        console.log('error catched')
    }
}

main()

We get an Uncaught (in promise) Error. This is expected since try/catch shouldn't handle the Promise rejection.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await#Description

The await expression causes async function execution to pause until a Promise is settled (that is, fulfilled or rejected), and to resume execution of the async function after fulfillment. When resumed, the value of the await expression is that of the fulfilled Promise.

If the Promise is rejected, the await expression throws the rejected value.

So if we are expecting the try/catch to handle the Promise rejection, we need to keep in mind two things:

  1. We need to call await so that the expression will throw when the Promise is rejected (avoiding an Uncaught (in Promise) Error)
  2. We must call await before the Promise rejection happens

We can see this in action by adding our first await:

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        const p1 = Stop(500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)

        await p1
    } catch (err) {
        console.log('error catched')
    }
}

main()

Now this await will throw once the Promise rejects and we avoid the Uncaught (in Promise) Error.

But if we add await p2 before await p1, the p1 Promise rejects during the await p2 and before await p1 gets called.The try/catch doesn't work in time for us to properly handle the Promise rejection:

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        const p1 = Stop(500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)

        await p2
        await p1
    } catch (err) {
        console.log('error catched')
    }
}

main()

We can further observe this key sequence of awaits by changing the time so that await p2 resumes the execution of the function in time for the await p1 to be called so that the try/catch is waiting for await p1 to throw.

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        // Increased the time for p1 so the Promise resolves after p2 Promise resolves
        const p1 = Stop(1500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)

        await p2
        await p1
    } catch (err) {
        console.log('error catched')
    }
}

main()

I'd suggest using Promise.all:

  1. Easier to manage catching those annoying Errors
  2. Avoids pausing multiple times for each await used (not an issue with your code snippets since the p1 Promise and p2 Promise are running in 'parallel', but this is a common issue found in code)

const main = async () => {

    const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time))

    try {
        const p1 = Stop(500).then(() => { throw new Error('Error ocurred') })
        const p2 = Stop(1000)

        await Promise.all([p2, p1])
    } catch (err) {
        console.log('error catched')
    }
}

main()

I modified your code a little bit and it works as expected.

const main = async () => {

const Stop = (time) => new Promise((resolve) => 
  setTimeout(resolve, time) )


try {
    const p1 = new Stop(500).then(() => { throw new Error('Error ocurred') })
    const p2 = new Stop(1000)

    await p2
    await p1
} catch (err) {
    console.log('error catched')
}} 
main()

What I understood is that Stop was referring to the same instance earlier and when I used new keyword it created 2 separate instances which solved the issue

The problem is that async/await is designed to launch an asynchronous task by time and wait for its execution, while in your snippets you are starting two asynchronous task simultaneously. If while waiting for one task execution to be finished the other one throws, you get the Uncaught (in promise) Error.

A good use of async/await is the following:

const main = async() => {

  const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time));

  try {
    const p1 = await Stop(500).then(() => { throw new Error('Error ocurred') });
    const p2 = await Stop(1000);
  } catch (err) {
    console.log('caught:', err.message)
  }
}

main()

but the two tasks are executed sequentially. The right way to deal with more simultaneous asynchronous tasks it Promise.all:

const main = async() => new Promise((resolve, reject) => {

  const Stop = (time) => new Promise((resolve) => setTimeout(resolve, time));

  const p1 = Stop(1000).then(() => {
    throw new Error('Error ocurred')
  });
  const p2 = Stop(3000);

  Promise.all([p1, p2]).then(resolve).catch(reject);
});

const parent = async() => {
  const start = new Date().getTime();
  try {
    console.log(await main());
  } catch (err) {
    console.log("caught:", err.message, "after:", new Date().getTime() - start, "ms");
  }
};

parent();

Related