Skip to content

Instantly share code, notes, and snippets.

@reillyeon
Last active February 22, 2022 16:05
Show Gist options
  • Save reillyeon/843bc82294443077d4ea21f527d9310e to your computer and use it in GitHub Desktop.
Save reillyeon/843bc82294443077d4ea21f527d9310e to your computer and use it in GitHub Desktop.
When does an async function return?

When does an async function return?

Someone recently asked me when an async function in JavaScript "returns". This is an interesting question because while a function like f() below has a return statement in it, f() actually returns much earlier than that. To understand how this works, let's take a look at an example function. Here, a() and c() are normal synchronous functions and b() is another asynchronous function.

async function f() {
  a();
  await b();
  return c();
}

Written using promises the actual code executed is more like,

function f() {
  return new Promise((resolve) => {
    a();
    resolve(b().then(() => {
      return c();
    }));
  });
}

An async function returns a Promise so it should be unsurprising to see new Promise() being called here. The function passed to the Promise constructor is invoked synchronously so before f() returns the code that has actually executed is,

a();
b().then(/* a function which isn't executed yet */);

So the synchonous result of f() is a new Promise that will resolve when the Promise returned by b().then() resolves. The remaining code after the await is captured in a new function that runs when the Promise returned by b() resolves. This code makes the eventual asynchronous result of f() whatever is returned by c().

Why does this matter?

When an asynchronous function is used as an event handler only the synchronous part of the function's execution happens before the event dispatch is complete. The difference between synchronous execution and asynchronous execution is important if the event handler calls a function like preventDefault(). Event dispatch is implemented as code similar to this,

function dispatchEvent() {
  const e = new Event();
  for (const handler of eventHandlers) {
    handler(e);
  }
  if (!e.defaultPrevented) {
    // Do the default action.
  }
}

Note that the event dispatch code doesn't await the result of handler(). This means that only the code that is executed synchronously within handler() gets a chance to run before it checks whether preventDefault() has been called. The following code therefore has a bug,

element.addEventListener('event', async (e) => {
  await fetch("/some-resource");
  e.preventDefault();
});

Since e.preventDefault() isn't called until the fetch() completes the event dispatch routine is long since complete and the default action was taken anyways. Writing it this way instead works as intended,

element.addEventListener('event', async (e) => {
  e.preventDefault();
  await fetch("/some-resource");
});

This can be a source of subtle bugs and is why I think it's important to understand how code using await is translated into code using promises.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment