Why does this async function run synchronously?

Viewed 124

Here's my code:

async function test() {
  console.log('TEST');
}

function go() {
  console.log('one');
  test();
  console.log('two');
}

go()

I've marked the test function as async, and I am not using await in the go method that calls it, so I expect the output to be this:

one
two
TEST

But the output comes out like this:

one
TEST
two

In my real use-case, test is a function that wraps some long-processing logic that I actually do want to happen asynchronously, not in-order.

Why isn't that happening here, and how can I fix it?

1 Answers

The body of an async function executes synchronously, until the first await statement. When it encounters an await expression it yields control to the calling function and is suspended. The part after the await expression is completed asynchronously.

As per the MDN docs:

The body of an async function can be thought of as being split by zero or more await expressions. Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously.

Code after each await expression can be thought of as existing in a .then callback. In this way a promise chain is progressively constructed with each reentrant step through the function. The return value forms the final link in the chain.

This can be observed with the following example with no await, where the body of the function is executed synchronously:

async function testSync() {
  console.log("Runs synchronously");
}


console.log(1);

testSync();

console.log(2);

Here is an example where an awaited Promise makes the behavior asynchronous after the await:

async function testAsync() {
  console.log("Runs synchronously until here");
  await myPromise();
  console.log("Runs asynchronously here");
}

function myPromise() {
  return Promise.resolve();
}


console.log(1);

testAsync();

console.log(2);

This could be made to execute asynchronously in a number of ways. You can wrap your function in a setTimeout call:

function test() {
  console.log("I want to execute asynchronously");
}

console.log(1);
setTimeout(test, 0);
console.log(2);

Or you can do it through promise callbacks:

function test(){
 console.log("I want to run asynchronously");
}
console.log(1);
Promise.resolve().then(test).catch(e => console.error(e));
console.log(2);

Same can be done using queueMicrotask, docs here :

function test(){
 console.log("I want to run asynchronously");
}
console.log(1);
queueMicrotask(test);
console.log(2);

The difference between the setTimeout and the method using Promise callbacks is how they are queued for execution. There are two task queues, one is the task queue where the initial program execution, setTimeout, setInterval, requestAnimationframe, etc callbacks are queued and the other is the microtask queue where promise callbacks are queued.

When you enqueue a task from the task queue and there are tasks still left, the event loop will check the microtask queue and execute them all before taking up the next task from the task queue.

Here is a snippet demonstrating this:

function testTaskQueue(){
 console.log("Enqueued in the task queue");
}
function testMicroTaskQueue(){
 console.log("Enqueued in the microtask queue");
}

console.log(1);
setTimeout(testTaskQueue, 0);
Promise.resolve().then(testMicroTaskQueue);
queueMicrotask(testMicroTaskQueue);
console.log(2)

So depending on this, you can make your own async wrapper and pass your function to be executes as a callback:

function makeAsync(callback, ...params) {
  //enqueued intask queue
  setTimeout(callback, 0, ...params);
  //Or using microtasks
  Promise.resolve().then(() => test(...params)).catch(console.error);
}

function test(...params) {
  console.log("I want to run asynchronoulsy", ...params);
}
console.log(1)
makeAsync(test, 3);
console.log(2);

Related