Home > Blockchain >  In JavaScript, when exactly does an async function return?
In JavaScript, when exactly does an async function return?

Time:12-16

I am intentionally not making this a snippet so readers can guess what the output is:

What would be printed out:

async function foo() {
  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });
  console.log("Right before return");
  return 123;
}

function takeIt(fn) {
  console.log("STARTING");
  foo();
  console.log("Function returned");
}

takeIt(foo);

and why?

Perspective one can be: Well, we all know async function will pause for the setTimeout and sleep for 3 seconds, and it won't return later on... so

STARTING
COMING INTO foo
Right before return
Function returned

Perspective two can be: Well, foo() returns 123 (or not return anything and that would mean to return undefined), and it is a promise... and returning a promise is instantaneous, so it is:

STARTING
Function returned
COMING INTO foo
Right before return

Now if we add one more tweak to the plot:

async function foo() {
  await Promise.resolve(678);

  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });
  console.log("Right before return");
  return 123;
}

function takeIt(fn) {
  console.log("STARTING");
  foo();
  console.log("Function returned");
}

takeIt(foo);

and there can be some more perspectives... I guessed what is printed for all 3 cases, before I ran the program, and got the correct answers. But I did not know precisely how it works, but I made a guess. Can somebody shred light on precisely how it works, and if needed, I will post my answer a few days later as to how I think it exactly worked.

The question is: when does foo() return and precisely, how does it work? What is the guiding principle?

CodePudding user response:

An async function will yield control flow back to its caller (that is, log Function returned in your example) once the async function's code runs into an await. It will not yield flow back before then. So here:

async function foo() {
  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });

Calling foo will immediately log COMING INTO foo, and then due to the await, control flow yields back to the caller of foo.

Similarly:

async function foo() {
  await Promise.resolve(678);

  console.log("COMING INTO foo");

The await comes first, so here, control flow is yielded back before foo logs anything.

Here, "control flow is yielded back" is synonymous with "returns a Promise to the caller."

CodePudding user response:

My answer is, at times it is difficult to tell exactly how an async function behaves, and here is the conversion:

async function foo() {
  console.log("COMING INTO foo");

  await new Promise((resolve) => {
    setTimeout(resolve, 3000);
  });
  console.log("Right before return");
  return 123;
}

function takeIt(fn) {
  console.log("STARTING");
  foo();
  console.log("Function returned");
}

takeIt(foo);

For the code above, it can be viewed exactly the same as:

function foo() {
  // any code before the first await goes here, which is executed immediately:
  console.log("COMING INTO foo");

  // when the first await is encountered, the whole thing becomes a promise
  // and is returned to the caller:
  return new Promise((resolve0, reject0) => {
    const promise0 = new Promise((resolve) => {
      setTimeout(resolve, 3000);
    });

    promise0.then(v0 => { 
      // the more await there are in the original code, the more "nested"
      // `then` there will be. It does indeed become a nesting hell
      console.log("Right before return");

      // no matter how deep the nesting is, the last returned value
      // is called with resolve0
      resolve0(123);
    });
  });
}

So the idea is: some code will be executed immediately, synchronously, but at the first point of await, it all of a sudden will become a gigantic promise, and its final resolved value would be the last line of the async function. So this "gigantic promise" is like floating in the air, waiting to be invoked and invoked again (we can think of it as handlers and handlers being invoked again and again, and are deeply nested).

In the async function, all the variables that get assigned the value from await are as if they are in the same scope, but in the converted code, the then handler can access those variables by using the outer scope, and so when the then handler is called, it is making use of closure to access these outer scope variables.

The purpose of this question and answer is so that we understand exactly what is happening, because I found some programmers, including myself previously before understanding this, had a vague idea of what is happening but cannot really tell, and writing code without really understanding it may not be a good practice.

  • Related