Search code examples
javascriptasynchronousasync-awaites6-promise

Why does "throw Error" make a function run as synchronous?


I was writing some code and found something interesting.

Example 1:

async function load(closure) {
    try {
      await closure();
    } catch (error) {
      console.log("error");
    } finally {
      console.log("finished");
    }
  }

  load(async () => {
    throw new Error();
  });

  console.log("hello");

This outputs:

hello
error
finished

However, if I call:

Example 2:

load(() => {
  throw new Error();
});

The output is:

error
finished
hello

Can you explain what's going on here?

I understand that calling await queues execution to the next tick, so the first output is expected.

But in the second example, why is closure behaving as a synchronous function if the caller is using await?

This only happens when there's an Error thrown. If I call...

Example 3:

load(() => {
    return 1;
});

... then closure is called in the next tick as expected.


Solution

  • Looking at the documentation

    The expression is resolved in the same way as Promise.resolve(): it's always converted to a native Promise and then awaited

    In your Example 2, await is not used because the error happens before the creation of the promise (i.e. during the execution of the closure function). The function doesn't return any value so there is no conversion to a native Promise.

    • If closure() is not async and throws an exception, the promise has not a chance to be created, that's why you see error and finished logs first. In fact await has not been used since closure function has not been fully executed.

    • If closure() is not async and returns a value, it's converted to a promise and the async function pauses until the next tick, that's why you see hello first.

    • If closure() is not async and returns a rejected promise like Promise.rejected(), you will see hello first because await received a rejected promise and the async function pauses until the next tick like before.

    function fn() {
      throw new Error();
    }
    
    async function load() {
      try {
        await fn(); // like await Promise.resolve(fn())
      } catch (error) {
        console.error("
            I came here because an error occurred during fn execution.
            No promise has been created and await has not been used.
            The async function has not been paused.
        ");
      }
    }
    
    function fn() {
      return Promise.reject("error");
    }
    // same as
    async function fn() {
      throw "error";
    }
    
    async function load() {
      try {
        await fn(); // like await Promise.reject()
      } catch (error) {
        console.error("
            I came here because await received a rejected promise.
            The async function has been paused until the next tick.
        ");
      }
    }